R/Ellipse.R

Defines functions maxAreaInscribedEllipse fitEllipse EllipseFromFociAndOnePoint EllipseFromThreeBoundaryPoints LownerJohnEllipse EllipseFromFivePoints EllipseFromEquation EllipseEquationFromFivePoints GaussianEllipse EllipseFromCenterAndMatrix

Documented in EllipseEquationFromFivePoints EllipseFromCenterAndMatrix EllipseFromEquation EllipseFromFivePoints EllipseFromFociAndOnePoint EllipseFromThreeBoundaryPoints fitEllipse GaussianEllipse LownerJohnEllipse maxAreaInscribedEllipse

#' @title R6 class representing an ellipse
#'
#' @description An ellipse is given by a center, two radii (\code{rmajor}
#' and \code{rminor}), and the angle (\code{alpha}) between the major axis and
#' the horizontal direction.
#'
#' @export
#' @importFrom R6 R6Class
#' @importFrom uniformly runif_in_ellipsoid runif_on_ellipsoid
#' @importFrom Carlson elliptic_E
Ellipse <- R6Class(

  "Ellipse",

  private = list(
    .center = c(NA_real_, NA_real_),
    .rmajor = NA_real_,
    .rminor = NA_real_,
    .alpha = NA_real_,
    .degrees = NA
  ),

  active = list(
    #' @field center get or set the center
    center = function(value) {
      if (missing(value)) {
        private[[".center"]]
      } else {
        center <- as.vector(value)
        stopifnot(
          is.numeric(center),
          length(center) == 2L,
          !any(is.na(center)),
          all(is.finite(center))
        )
        private[[".center"]] <- center
      }
    },

    #' @field rmajor get or set the major radius of the ellipse
    rmajor = function(value) {
      if (missing(value)) {
        private[[".rmajor"]]
      } else {
        rmajor <- as.vector(value)
        rminor <- private[[".rminor"]]
        stopifnot(
          is.numeric(rmajor),
          length(rmajor) == 1L,
          !is.na(rmajor),
          is.finite(rmajor),
          rmajor > 0,
          rmajor >= rminor
        )
        private[[".rmajor"]] <- rmajor
      }
    },

    #' @field rminor get or set the minor radius of the ellipse
    rminor = function(value) {
      if (missing(value)) {
        private[[".rminor"]]
      } else {
        rminor <- as.vector(value)
        rmajor <- private[[".rmajor"]]
        stopifnot(
          is.numeric(rminor),
          length(rminor) == 1L,
          !is.na(rminor),
          is.finite(rminor),
          rminor > 0,
          rminor <= rmajor
        )
        private[[".rminor"]] <- rminor
      }
    },

    #' @field alpha get or set the angle of the ellipse
    alpha = function(value) {
      if (missing(value)) {
        private[[".alpha"]]
      } else {
        alpha <- as.vector(value)
        stopifnot(
          is.numeric(alpha),
          length(alpha) == 1L,
          !is.na(alpha),
          is.finite(alpha)
        )
        private[[".alpha"]] <- alpha
      }
    },

    #' @field degrees get or set the \code{degrees} field
    degrees = function(value) {
      if (missing(value)) {
        private[[".degrees"]]
      } else {
        degrees <- as.vector(value)
        stopifnot(
          is.logical(degrees),
          length(degrees) == 1L,
          !is.na(degrees)
        )
        private[[".degrees"]] <- degrees
      }
    }
  ),

  public = list(
    #' @description Create a new \code{Ellipse} object.
    #' @param center a point, the center of the rotation
    #' @param rmajor positive number, the major radius
    #' @param rminor positive number, the minor radius
    #' @param alpha a number, the angle between the major axis and the
    #' horizontal direction
    #' @param degrees logical, whether \code{alpha} is given in degrees
    #' @return A new \code{Ellipse} object.
    #' @examples Ellipse$new(c(1,1), 3, 2, 30)
    initialize = function(center, rmajor, rminor, alpha, degrees = TRUE) {
      center <- as.vector(center)
      stopifnot(
        is.numeric(center),
        length(center) == 2L,
        !any(is.na(center)),
        all(is.finite(center))
      )
      rmajor <- as.vector(rmajor)
      stopifnot(
        is.numeric(rmajor),
        length(rmajor) == 1L,
        !is.na(rmajor),
        is.finite(rmajor),
        rmajor > 0
      )
      rminor <- as.vector(rminor)
      stopifnot(
        is.numeric(rminor),
        length(rminor) == 1L,
        !is.na(rminor),
        is.finite(rminor),
        rminor > 0,
        rminor <= rmajor
      )
      alpha <- as.vector(alpha)
      stopifnot(
        is.numeric(alpha),
        length(alpha) == 1L,
        !is.na(alpha),
        is.finite(alpha)
      )
      degrees <- as.vector(degrees)
      stopifnot(
        is.logical(degrees),
        length(degrees) == 1L,
        !is.na(degrees)
      )
      private[[".center"]] <- center
      private[[".rmajor"]] <- rmajor
      private[[".rminor"]] <- rminor
      private[[".alpha"]] <- alpha
      private[[".degrees"]] <- degrees
    },

    #' @description Show instance of an \code{Ellipse} object.
    #' @param ... ignored
    print = function(...) {
      private[[".center"]] -> center
      private[[".rmajor"]] -> rmajor
      private[[".rminor"]] -> rminor
      private[[".alpha"]] -> alpha
      private[[".degrees"]] -> degrees
      cat("Ellipse:\n")
      cat("       center: ", toString(center), "\n", sep = "")
      cat(" major radius: ", toString(rmajor), "\n", sep = "")
      cat(" minor radius: ", toString(rminor), "\n", sep = "")
      cat("        angle: ",
          sprintf("%s %s", alpha,
                  ifelse(degrees,
                         ifelse(alpha %in% c(0,1,-1), "degree", "degrees"),
                         ifelse(alpha %in% c(0,1,-1), "radian", "radians"))
          ), "\n", sep = "")
    },

    #' @description Check whether the reference ellipse equals an ellipse.
    #' @param ell An \code{Ellipse} object.
    isEqual = function(ell){
      if(is(ell, "Circle")) ell <- .circleAsEllipse(ell)
      private[[".center"]] -> center0
      private[[".rmajor"]] -> rmajor0
      private[[".rminor"]] -> rminor0
      private[[".alpha"]] -> alpha0
      private[[".degrees"]] -> degrees
      if(!degrees) alpha0 <- (alpha0 * 180/pi)
      alpha1 <- ell$alpha
      if(!ell$degrees) alpha1 <- (alpha1 * 180/pi)
      isTRUE(all.equal(
        c(center0, rmajor0, rminor0, alpha0 %% 180),
        c(ell$center, ell$rmajor, ell$rminor, alpha1 %% 180)
      ))
    },

    #' @description The coefficients of the implicit equation of the ellipse.
    #' @return A named numeric vector.
    #' @details The implicit equation of the ellipse is
    #' \code{Ax² + Bxy + Cy² + Dx + Ey + F = 0}. This method returns
    #' A, B, C, D, E and F.
    equation = function(){
      private[[".center"]] -> center
      private[[".rmajor"]]^2 -> a2
      private[[".rminor"]]^2 -> b2
      private[[".alpha"]] -> alpha
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      x <- center[1L]; y <- center[2L]
      sine <- sin(alpha); cosine <- cos(alpha)
      sine2 <- sine*sine; cosine2 <- 1-sine2
      A <- a2*sine2 + b2*cosine2
      B <- 2*(b2-a2)*sine*cosine
      C <- a2*cosine2 + b2*sine2
      D <- -2*A*x - B*y
      E <- -B*x - 2*C*y
      F <- A*x*x + B*x*y + C*y*y - a2*b2
      c(A = A, B = B, C = C, D = D, E = E, F = F)
    },

    #' @description Check whether a point lies on the reference ellipse.
    #' @param M a point
    includes = function(M){
      ABCDEF <- as.list(self$equation())
      x <- M[1L]; y <- M[2L]
      zero <- with(ABCDEF, A*x*x + B*x*y + C*y*y + D*x + E*y + F)
      isTRUE(all.equal(0, zero, check.attributes=FALSE))
    },

    #' @description Check whether a point is contained in the reference ellipse.
    #' @param M a point
    contains = function(M){
      x <- M[1L]; y <- M[2L]
      ABCDEF <- as.list(self$equation())
      with(ABCDEF, A*x*x + B*x*y + C*y*y + D*x + E*y + F) <= 0
    },

    #' @description Returns the 2x2 matrix \code{S} associated to the reference
    #' ellipse. The equation of the ellipse is \code{t(M-O) \%*\% S \%*\% (M-O) = 1}.
    #' @examples ell <- Ellipse$new(c(1,1), 5, 1, 30)
    #' S <- ell$matrix()
    #' O <- ell$center
    #' pts <- ell$path(4L) # four points on the ellipse
    #' apply(pts, 1L, function(M) t(M-O) %*% S %*% (M-O))
    matrix = function(){
      ABCDEF <- as.list(self$equation())
      X <- with(ABCDEF, cbind(
        c(A, B/2, D/2),
        c(B/2, C, E/2),
        c(D/2, E/2, F)))
      K <- -det(X) / with(ABCDEF, A*C - B*B/4)
      X[-3L,-3L] / K
    },

    #' @description Path that forms the reference ellipse.
    #' @param npoints number of points of the path
    #' @param closed Boolean, whether to return a closed path; you don't need
    #'   a closed path if you want to plot it with
    #'   \code{\link[graphics:polygon]{polygon}}
    #' @param outer Boolean; if \code{TRUE}, the ellipse will be contained
    #'   inside the path, otherwise it will contain the path
    #' @return A matrix with two columns \code{x} and \code{y} of
    #'   length \code{npoints}.
    #' @examples
    #' library(PlaneGeometry)
    #' ell <- Ellipse$new(c(1, -1), rmajor = 3, rminor = 2, alpha = 30)
    #' innerPath <- ell$path(npoints = 10)
    #' outerPath <- ell$path(npoints = 10, outer = TRUE)
    #' bbox <- ell$boundingbox()
    #' plot(NULL, asp = 1, xlim = bbox$x, ylim = bbox$y, xlab = NA, ylab = NA)
    #' draw(ell, border = "red", lty = "dashed")
    #' polygon(innerPath, border = "blue", lwd = 2)
    #' polygon(outerPath, border = "green", lwd = 2)
    path = function(npoints = 100L, closed = FALSE, outer = FALSE){
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      if(outer) {
        .ellipsePointsOuter(
          closed,
          private[[".center"]],
          private[[".rmajor"]],
          private[[".rminor"]],
          alpha,
          as.integer(npoints)
        )
      } else {
        if(closed) {
          t_ <- seq(0, 2*pi, length.out = npoints)
        } else {
          t_ <- seq(0, 2*pi, length.out = npoints+1L)[-1L]
        }
        .ellipsePoints(
          t_,
          private[[".center"]],
          private[[".rmajor"]],
          private[[".rminor"]],
          alpha
        )
      }
    },

    #' @description Diameter and conjugate diameter of the reference ellipse.
    #' @param t a number, the diameter only depends on \code{t} modulo
    #' \code{pi}; the axes correspond to \code{t=0} and \code{t=pi/2}
    #' @param conjugate logical, whether to return the conjugate diameter as well
    #' @return A \code{Line} object or a list of two \code{Line} objects if
    #' \code{conjugate = TRUE}.
    #' @examples ell <- Ellipse$new(c(1,1), 5, 2, 30)
    #' diameters <- lapply(c(0, pi/3, 2*pi/3), ell$diameter)
    #' plot(NULL, asp = 1, xlim = c(-4,6), ylim = c(-2,4),
    #'      xlab = NA, ylab = NA)
    #' draw(ell)
    #' invisible(lapply(diameters, draw))
    diameter = function(t, conjugate = FALSE){
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      ts <- if(conjugate){
        c(t, t+pi, t+pi/2, t-pi/2)
      }else{
        c(t, t+pi)
      }
      pts <- .ellipsePoints(
        ts,
        private[[".center"]],
        private[[".rmajor"]],
        private[[".rminor"]],
        alpha
      )
      if(conjugate){
        list(
          Line$new(pts[1L,], pts[2L,], FALSE, FALSE),
          Line$new(pts[3L,], pts[4L,], FALSE, FALSE)
        )
      }else{
        Line$new(pts[1L,], pts[2L,], FALSE, FALSE)
      }
    },

    #' @description Perimeter of the reference ellipse.
    perimeter = function() {
      a <- private[[".rmajor"]]
      b <- private[[".rminor"]]
      4 * a * Re(elliptic_E(pi/2, 1-b^2/a^2, minerror = 1e-12))
    },

    #' @description Intersection point of the ellipse with the half-line
    #' starting at the ellipse center and forming angle \code{theta} with
    #' the major axis.
    #' @param theta a number, the angle, or a numeric vector
    #' @param degrees logical, whether \code{theta} is given in degrees
    #' @return A point of the ellipse if \code{length(theta)==1} or a
    #' two-column matrix of points of the ellipse if
    #' \code{length(theta) > 1} (one point per row).
    pointFromAngle = function(theta, degrees = TRUE){
      theta <- as.vector(theta)
      stopifnot(
        is.numeric(theta),
        length(theta) >= 1L,
        !any(is.na(theta)),
        all(is.finite(theta))
      )
      O <- private[[".center"]]
      a <- private[[".rmajor"]]
      b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      if(degrees) theta <- theta * pi/180
      #t <- sort(atan2(a * tan(theta), b) %% (2*pi))
      sgn <- ifelse(theta %% (2*pi) <= sqrt(.Machine$double.eps), 1, -1)
      t <- atan2(a/b, 1/tan(theta %% (2*pi))) +
        theta + sgn*sqrt(.Machine$double.eps) -
        (theta + sgn*sqrt(.Machine$double.eps)) %% pi
      # t <- ifelse(theta <= pi,
      #             atan2(a/b,1/tan(theta)),
      #             ifelse(theta <= 2*pi,
      #                    atan2(a/b,1/tan(theta)) + pi,
      #                    atan2(a/b,1/tan(theta)) + 2*pi))
      out <- .ellipsePoints(t, O, a, b, alpha)
      if(length(theta) == 1L) out <- c(out)
      out
    },

    #' @description Point of the ellipse with given eccentric angle.
    #' @param t a number, the eccentric angle in radians, or a numeric vector
    #' @return A point of the ellipse if \code{length(t)==1} or a
    #' two-column matrix of points of the ellipse if
    #' \code{length(t) > 1} (one point per row).
    pointFromEccentricAngle = function(t){
      t <- as.vector(t)
      stopifnot(
        is.numeric(t),
        length(t) >= 1L,
        !any(is.na(t)),
        all(is.finite(t))
      )
      O <- private[[".center"]]
      a <- private[[".rmajor"]]
      b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      out <- .ellipsePoints(t, O, a, b, alpha)
      if(length(t) == 1L) out <- c(out)
      out
    },

    #' @description Semi-major axis of the ellipse.
    #' @return A segment (\code{Line} object).
    semiMajorAxis = function(){
      O <- private[[".center"]]
      a <- private[[".rmajor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      O_A <- a * c(cos(alpha), sin(alpha))
      Line$new(O, O + O_A, FALSE, FALSE)
    },

    #' @description Semi-minor axis of the ellipse.
    #' @return A segment (\code{Line} object).
    semiMinorAxis = function(){
      O <- private[[".center"]]
      b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      O_B <- b * c(-sin(alpha), cos(alpha))
      Line$new(O, O + O_B, FALSE, FALSE)
    },


    #' @description Foci of the reference ellipse.
    #' @return A list with the two foci.
    foci = function(){
      O <- private[[".center"]]
      a <- private[[".rmajor"]]; b <- private[[".rminor"]]
      e <- sqrt(1 - b*b/a/a)
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      O_A <- a * c(cos(alpha), sin(alpha))
      list(F1 = O + e*O_A, F2 = O - e*O_A)
    },

    #' @description Tangents of the reference ellipse at a point given by
    #' its eccentric angle.
    #' @param t eccentric angle, there is one tangent for each value of \code{t}
    #' modulo \code{2*pi}; for \code{t = 0, pi/2, pi, -pi/2}, these are the
    #' tangents at the vertices of the ellipse
    #' @examples ell <- Ellipse$new(c(1,1), 5, 2, 30)
    #' tangents <- lapply(c(0, pi/3, 2*pi/3, pi, 4*pi/3, 5*pi/3), ell$tangent)
    #' plot(NULL, asp = 1, xlim = c(-4,6), ylim = c(-2,4),
    #'      xlab = NA, ylab = NA)
    #' draw(ell, col = "yellow")
    #' invisible(lapply(tangents, draw, col = "blue"))
    tangent = function(t){
      t <- as.vector(t)
      stopifnot(
        is.numeric(t),
        length(t) == 1L,
        !is.na(t),
        is.finite(t)
      )
      O <- private[[".center"]]
      a <- private[[".rmajor"]]; b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      x <- a*cos(t); y <- b*sin(t)
      cosalpha <- cos(alpha); sinalpha <- sin(alpha)
      T <- c(
        O[1L] + cosalpha*x - sinalpha*y,
        O[2L] + sinalpha*x + cosalpha*y
      )
      x <- -a*sin(t); y <- b*cos(t)
      v <- c(
        cosalpha*x - sinalpha*y,
        sinalpha*x + cosalpha*y
      )
      Line$new(T, T+v)
    },

    #' @description Normal unit vector to the ellipse.
    #' @param t a number, the eccentric angle in radians of the point of the
    #' ellipse at which we want the normal unit vector
    #' @return The normal unit vector to the ellipse at the point given by
    #' eccentric angle \code{t}.
    #' @examples ell <- Ellipse$new(c(1,1), 5, 2, 30)
    #' t_ <- seq(0, 2*pi, length.out = 13)[-1]
    #' plot(NULL, asp = 1, xlim = c(-5,7), ylim = c(-3,5),
    #'      xlab = NA, ylab = NA)
    #' draw(ell, col = "magenta")
    #' for(i in 1:length(t_)){
    #'   t <- t_[i]
    #'   P <- ell$pointFromEccentricAngle(t)
    #'   v <- ell$normal(t)
    #'   draw(Line$new(P, P+v, FALSE, FALSE))
    #' }
    normal = function(t){
      t <- as.vector(t)
      stopifnot(
        is.numeric(t),
        length(t) == 1L,
        !is.na(t),
        is.finite(t)
      )
      O <- private[[".center"]]
      a <- private[[".rmajor"]]; b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      cosalpha <- cos(alpha); sinalpha <- sin(alpha)
      x <- -a*sin(t); y <- b*cos(t)
      v <- c(
        sinalpha*x + cosalpha*y,
        -cosalpha*x + sinalpha*y
      )
      v / .vlength(v)
    },

    #' @description Convert angle to eccentric angle.
    #' @param theta angle between the major axis and the half-line starting
    #' at the center of the ellipse and passing through the point of interest
    #' on the ellipse
    #' @param degrees logical, whether \code{theta} is given in degrees
    #' @return The eccentric angle of the point of interest on the ellipse,
    #' in radians.
    #' @examples O <- c(1, 1)
    #' ell <- Ellipse$new(O, 5, 2, 30)
    #' theta <- 20
    #' P <- ell$pointFromAngle(theta)
    #' t <- ell$theta2t(theta)
    #' tg <- ell$tangent(t)
    #' OP <- Line$new(O, P, FALSE, FALSE)
    #' plot(NULL, asp = 1, xlim = c(-4,6), ylim = c(-2,5),
    #'      xlab = NA, ylab = NA)
    #' draw(ell, col = "antiquewhite")
    #' points(P[1], P[2], pch = 19)
    #' draw(tg, col = "red")
    #' draw(OP)
    #' draw(ell$semiMajorAxis())
    #' text(t(O+c(1,0.9)), expression(theta))
    theta2t = function(theta, degrees = TRUE){
      theta <- as.vector(theta)
      stopifnot(
        is.numeric(theta),
        length(theta) == 1L,
        !is.na(theta),
        is.finite(theta)
      )
      a <- private[[".rmajor"]]; b <- private[[".rminor"]]
      if(degrees) theta <- theta * pi/180
      sgn <- ifelse(theta %% (2*pi) <= sqrt(.Machine$double.eps), 1, -1)
      atan2(a/b, 1/tan(theta %% (2*pi))) +
        theta + sgn*sqrt(.Machine$double.eps) -
        (theta + sgn*sqrt(.Machine$double.eps)) %% pi
    },

    #' @description Regression lines. The regression line of y on x intersects
    #' the ellipse at its rightmost point and its leftmost point.
    #' The tangents at these points are vertical.
    #' The regression line of x on y intersects the ellipse at its
    #' topmost point and its bottommost point.
    #' The tangents at these points are horizontal.
    #' @return A list with two \code{Line} objects:
    #' the regression line of y on x and the regression line of x on y.
    #' @examples ell <- Ellipse$new(c(1,1), 5, 2, 30)
    #' reglines <- ell$regressionLines()
    #' plot(NULL, asp = 1, xlim = c(-4,6), ylim = c(-2,4),
    #'      xlab = NA, ylab = NA)
    #' draw(ell, lwd = 2)
    #' draw(reglines$YonX, lwd = 2, col = "blue")
    #' draw(reglines$XonY, lwd = 2, col = "green")
    regressionLines = function(){
      O <- private[[".center"]]
      a <- private[[".rmajor"]]; b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      cosalpha <- cos(alpha); sinalpha <- sin(alpha)
      A <- -b*sinalpha; B <- -a*cosalpha
      t1 <- .solveTrigonometricEquation(A, B)
      A <- b*cosalpha; B <- -a*sinalpha
      t2 <- .solveTrigonometricEquation(A, B)
      pts <- .ellipsePoints(c(t1,t2), O, a, b, alpha)
      list(
        YonX = Line$new(pts[1L,], pts[2L,], FALSE, FALSE),
        XonY = Line$new(pts[3L,], pts[4L,], FALSE, FALSE)
      )
    },

    #' @description Return the smallest rectangle parallel to the axes
    #' which contains the reference ellipse.
    #' @return A list with two components: the x-limits in \code{x} and the
    #' y-limits in \code{y}.
    #' @examples ell <- Ellipse$new(c(2,2), 5, 3, 40)
    #' box <- ell$boundingbox()
    #' plot(NULL, asp = 1, xlim = box$x, ylim = box$y, xlab = NA, ylab = NA)
    #' draw(ell, col = "seaShell", border = "blue")
    #' abline(v = box$x, lty = 2); abline(h = box$y, lty = 2)
    boundingbox = function(){
      O <- private[[".center"]]
      a <- private[[".rmajor"]]; b <- private[[".rminor"]]
      alpha <- private[[".alpha"]]
      if(private[[".degrees"]]) alpha <- alpha * pi/180
      cosalpha <- cos(alpha); sinalpha <- sin(alpha)
      A <- -b*sinalpha; B <- -a*cosalpha
      t1 <- .solveTrigonometricEquation(A, B)
      A <- b*cosalpha; B <- -a*sinalpha
      t2 <- .solveTrigonometricEquation(A, B)
      pts <- .ellipsePoints(c(t1,t2), O, a, b, alpha)
      list(
        x = sort(c(pts[1L,1L], pts[2L,1L])),
        y = sort(c(pts[3L,2L], pts[4L,2L]))
      )
    },

    #' @description Random points on or in the reference ellipse.
    #' @param n an integer, the desired number of points
    #' @param where \code{"in"} to generate inside the ellipse,
    #' \code{"on"} to generate on the ellipse
    #' @return The generated points in a two columns matrix with \code{n} rows.
    #' @examples ell <- Ellipse$new(c(1,1), 5, 2, 30)
    #' pts <- ell$randomPoints(100)
    #' plot(NULL, type="n", asp=1, xlim = c(-4,6), ylim = c(-2,4),
    #'      xlab = NA, ylab = NA)
    #' draw(ell, lwd = 2)
    #' points(pts, pch = 19, col = "blue")
    randomPoints = function(n, where = "in"){
      where <- match.arg(where, c("in", "on"))
      S <- self$matrix()
      if(where == "in"){
        sims <- runif_in_ellipsoid(n, S, 1)
        sweep(sims, 2L, private[[".center"]], "+")
      }else{
        sims <- runif_on_ellipsoid(n, S, 1)
        sweep(sims, 2L, private[[".center"]], "+")
      }
    }
  )
)

#' Ellipse from center and matrix
#'
#' @description Returns the ellipse of equation
#' \code{t(X-center) \%*\% S \%*\% (X-center) = 1}.
#'
#' @param center a point, the center of the ellipse
#' @param S a positive symmetric matrix
#'
#' @return An \code{Ellipse} object.
#' @export
#'
#' @examples ell <- Ellipse$new(c(2,3), 4, 2, 20)
#' S <- ell$matrix()
#' EllipseFromCenterAndMatrix(ell$center, S)
EllipseFromCenterAndMatrix <- function(center, S){
  stopifnot(isSymmetric(S))
  e <- eigen(S, symmetric = TRUE)
  if(any(e$values <= 0)) stop("`S` is not positive.")
  .EllipseFromCenterAndEigen(center, e)
}


#' Gaussian ellipse
#' @description Return the ellipse equal to the highest \emph{pdf} region of
#' a bivariate Gaussian distribution with a given probability.
#'
#' @param mean numeric vector of length 2, the mean of the bivariate Gaussian
#' distribution; this is the center of the ellipse
#' @param Sigma covariance matrix of the bivariate Gaussian distribution
#' @param p desired probability level, a number between 0 and 1 (strictly)
#'
#' @return An \code{Ellipse} object.
#' @export
GaussianEllipse <- function(mean, Sigma, p){
  stopifnot(
    isSymmetric(Sigma),
    p > 0, p < 1
  )
  e <- eigen(Sigma, symmetric = TRUE)
  if(any(e$values <= 0)) stop("`Sigma` is not positive.")
  r <- -2 * log1p(-p)
  e <- list(
    values = rev(1/e$values)/r,
    vectors = e$vectors %*% cbind(c(0,1),c(-1,0))
  )
  .EllipseFromCenterAndEigen(mean, e)
}

#' Ellipse equation from five points
#' @description The coefficients of the implicit equation of an ellipse from
#' five points on this ellipse.
#' @param P1,P2,P3,P4,P5 the five points
#' @return A named numeric vector.
#' @export
#' @details The implicit equation of the ellipse is
#' \code{Ax² + Bxy + Cy² + Dx + Ey + F = 0}. This function returns
#' A, B, C, D, E and F.
#' @examples ell <- Ellipse$new(c(2,3), 5, 4, 30)
#' set.seed(666)
#' pts <- ell$randomPoints(5, "on")
#' cf1 <- EllipseEquationFromFivePoints(pts[1,],pts[2,],pts[3,],pts[4,],pts[5,])
#' cf2 <- ell$equation() # should be the same up to a multiplicative factor
#' all.equal(cf1/cf1["F"], cf2/cf2["F"])
EllipseEquationFromFivePoints <- function(P1, P2, P3, P4, P5){
  P <- rbind(P1, P2, P3, P4, P5)
  if(anyDuplicated(P)) stop("The five points are not distinct.")
  x <- P[,1L]; y <- P[,2L]
  M <- cbind(x*x, x*y, y*y, x, y, 1)
  A <- det(M[,-1L])
  B <- -det(M[,-2L])
  C <- det(M[,-3L])
  if(B*B-4*A*C >= 0) stop("The five points do not lie on an ellipse.")
  D <- -det(M[,-4L])
  E <- det(M[,-5L])
  F <- -det(M[,-6L])
  c(A = A, B = B, C = C, D = D, E = E, F = F)
}


#' Ellipse from its implicit equation
#' @description Return an ellipse from the coefficients of its implicit equation.
#' @param A,B,C,D,E,F the coefficients of the equation
#' @return An \code{Ellipse} object.
#' @export
#' @details The implicit equation of the ellipse is
#' \code{Ax² + Bxy + Cy² + Dx + Ey + F = 0}. This function returns the ellipse
#' given A, B, C, D, E and F.
#' @examples ell <- Ellipse$new(c(2,3), 5, 4, 30)
#' cf <- ell$equation()
#' ell2 <- EllipseFromEquation(cf[1], cf[2], cf[3], cf[4], cf[5], cf[6])
#' ell$isEqual(ell2)
EllipseFromEquation <- function(A, B, C, D, E, F){
  stopifnot(A*C > 0)
  if(B*B-4*A*C >= 0) stop("These parameters do not define an ellipse.")
  #if(D*D + E*E <= 4*(A+C)*F) stop("These parameters do not define an ellipse.")
  Q <- rbind(c(2*A, B, D), c(B, 2*C, E), c(D, E, 2*F))
  if(det(Q) == 0) stop("These parameters do not define an ellipse.")
  M0 <- matrix(c(F, D/2, E/2, D/2, A, B/2, E/2, B/2, C), 3L, 3L)
  M <- matrix(c(A, B/2, B/2, C), 2L, 2L)
  lambda <- eigen(M, symmetric = TRUE)$values
  #if(abs(lambda[1L] - A) >= abs(lambda[1L] - C)) lambda <- rev(lambda)
  detM0 <- det(M0); detM <- det(M)
  a <- sqrt(-detM0 / (detM*lambda[1L]))
  b <- sqrt(-detM0 / (detM*lambda[2L]))
  x <- B*E - 2*C*D
  y <- B*D - 2*A*E
  phi <- if(is.nan(B/(A-C))){
    0
  }else{
    if(abs(C) > abs(A)) atan(B/(A-C))/2 else (pi/2 - atan(-B/(A-C))/2)
  }
  Ellipse$new(c(x,y)/(4*A*C - B*B), max(a,b), min(a,b), (phi*180/pi) %% 180)
}

#' Ellipse from five points
#' @description Return an ellipse from five given points on this ellipse.
#' @param P1,P2,P3,P4,P5 the five points
#' @return An \code{Ellipse} object.
#' @export
#' @examples ell <- Ellipse$new(c(2,3), 5, 4, 30)
#' set.seed(666)
#' pts <- ell$randomPoints(5, "on")
#' ell2 <- EllipseFromFivePoints(pts[1,],pts[2,],pts[3,],pts[4,],pts[5,])
#' ell$isEqual(ell2)
EllipseFromFivePoints <- function(P1, P2, P3, P4, P5){
  cf <- EllipseEquationFromFivePoints(P1, P2, P3, P4, P5)
  EllipseFromEquation(cf[1L], cf[2L], cf[3L], cf[4L], cf[5L], cf[6L])
}


#' Löwner-John ellipse (ellipse hull)
#' @description Minimum area ellipse containing a set of points.
#' @param pts the points in a two-columns matrix (one point per row); at least
#' three distinct points
#' @return An \code{Ellipse} object.
#' @export
#' @examples \donttest{pts <- cbind(rnorm(30, sd=2), rnorm(30))
#' ell <- LownerJohnEllipse(pts)
#' box <- ell$boundingbox()
#' plot(NULL, asp = 1, xlim = box$x, ylim = box$y, xlab = NA, ylab = NA)
#' draw(ell, col = "seaShell")
#' points(pts, pch = 19)
#' all(apply(pts, 1, ell$contains)) # should be TRUE}
LownerJohnEllipse <- function(pts){
  stopifnot(
    is.matrix(pts),
    is.numeric(pts),
    ncol(pts) == 2L,
    nrow(pts[!duplicated(pts),]) >= 3L,
    !any(is.na(pts)),
    all(is.finite(pts))
  )
  y <- sdpt3r::minelips(pts)$y
  B <- diag(y[c(1L,2L)])
  B[2L,1L] <- B[1L,2L] <- y[3L]
  EllipseFromCenterAndMatrix(-c(chol2inv(chol(B)) %*% y[c(4L,5L)]),
                             tcrossprod(B))
}

#' @title Smallest ellipse that passes through three boundary points
#' @description Returns the smallest area ellipse which passes through
#'   three given boundary points.
#'
#' @param P1,P2,P3 three non-collinear points
#'
#' @return An \code{Ellipse} object.
#' @export
#'
#' @examples
#' P1 <- c(-1,0); P2 <- c(0, 2); P3 <- c(3,0)
#' ell <- EllipseFromThreeBoundaryPoints(P1, P2, P3)
#' ell$includes(P1); ell$includes(P2); ell$includes(P3)
EllipseFromThreeBoundaryPoints <- function(P1, P2, P3){
  if(.collinear(P1, P2, P3)){
    stop("The three points are collinear.")
  }
  points <- rbind(P1, P2, P3)
  means <- colMeans(points)
  cpoints <- sweep(points, 2L, means)
  S <- 1.5 * solve(crossprod(cpoints))
  EllipseFromCenterAndMatrix(means, S)
}

#' @title Ellipse from foci and one point
#' @description Derive the ellipse with given foci and one point on the boundary.
#'
#' @param F1,F2 points, the foci
#' @param P a point on the boundary of the ellipse
#'
#' @return An \code{Ellipse} object.
#' @export
EllipseFromFociAndOnePoint <- function(F1, F2, P){
  k <- .distance(P, F1) + .distance(P, F2)
  a <- k/2
  center <- (F1 + F2) / 2
  d <- .distance(center, F1)
  b <- sqrt(a*a - d*d)
  alpha <- atan(abs(F1[2]-F2[2])/abs(F1[1]-F2[1]))
  Ellipse$new(center, a, b, alpha, degrees = FALSE)
}


#' @title Fit an ellipse
#' @description Fit an ellipse to a set of points.
#'
#' @param points numeric matrix with two columns, one point per row
#'
#' @return An \code{Ellipse} object representing the fitted ellipse. The
#'   residual sum of squares is given in the \code{RSS} attribute.
#' @export
#' @importFrom fitConic fitConic
#'
#' @examples library(PlaneGeometry)
#' # We add some noise to 30 points on an ellipse:
#' ell <- Ellipse$new(c(1, 1), 3, 2, 30)
#' set.seed(666L)
#' points <- ell$randomPoints(30, "on") + matrix(rnorm(30*2, sd = 0.2), ncol = 2)
#' # Now we fit an ellipse to these points:
#' ellFitted <- fitEllipse(points)
#' # let's draw all this stuff:
#' box <- ell$boundingbox()
#' plot(NULL, asp = 1, xlim = box$x, ylim = box$y, xlab = NA, ylab = NA)
#' draw(ell, border = "blue", lwd = 2)
#' points(points, pch = 19)
#' draw(ellFitted, border = "green", lwd = 2)
fitEllipse <- function(points){
  if(!is.matrix(points) || !is.numeric(points)){
    stop("The `points` argument must be a numeric matrix.", call. = TRUE)
  }
  if(ncol(points) != 2L){
    stop("The `points` matrix must have two columns.", call. = TRUE)
  }
  if(any(is.na(points))){
    stop("Points with missing values are not allowed.", call. = TRUE)
  }
  fit <- fitConic(points, conicType = "e")
  if(fit[["exitCode"]] != 1){
    stop("The ellipse fitting has failed.", call. = TRUE)
  }
  cfs <- fit[["parA"]]
  fittedEllipse <- EllipseFromEquation(
    cfs[1L], cfs[2L], cfs[3L], cfs[4L], cfs[5L], cfs[6L]
  )
  attr(fittedEllipse, "RSS") <- fit[["RSS"]]
  fittedEllipse
}

#' @title Maximum area ellipse inscribed in a convex polygon
#' @description Computes the ellipse inscribed in a convex polygon with
#'   maximum area.
#'
#' @param points the vertices of the polygon in a two-columns matrix; their
#'   order has no importance, since the procedure takes the convex hull of
#'   these points (and does not check the convexity)
#' @param verbose argument passed to \code{\link[CVXR:psolve]{psolve}}
#'
#' @return An \code{Ellipse} object. The status of the optimization problem
#'   is given as an attribute of this ellipse. A warning is thrown if it is
#'   not optimal.
#' @export
#' @seealso \code{\link{maxAreaInscribedCircle}}
#' @importFrom rcdd makeV scdd
#' @importFrom CVXR Variable Minimize log_det norm2 Problem psolve
#' @examples
#' hexagon <- rbind(
#'   c(-1.7, -1),
#'   c(-1.4, 0.4),
#'   c(0.3, 1.3),
#'   c(1.7, 0.6),
#'   c(1.3, -0.3),
#'   c(-0.4, -1.8)
#' )
#' opar <- par(mar = c(2, 2, 1, 1))
#' plot(NULL, xlim=c(-2, 2), ylim=c(-2, 2), xlab = NA, ylab = NA, asp = 1)
#' points(hexagon, pch = 19)
#' polygon(hexagon)
#' ell <- maxAreaInscribedEllipse(hexagon)
#' draw(ell, col = "yellow2", border = "blue", lwd = 2)
#' par(opar)
#' # check optimization status:
#' attr(ell, "status")
maxAreaInscribedEllipse <- function(points, verbose = FALSE) {
  if(!is.matrix(points) || !is.numeric(points)){
    stop("The `points` argument must be a numeric matrix.", call. = TRUE)
  }
  if(ncol(points) != 2L){
    stop("The `points` matrix must have two columns.", call. = TRUE)
  }
  if(nrow(points) < 3L){
    stop("The `points` matrix must have at least three rows.", call. = TRUE)
  }
  if(any(is.na(points))){
    stop("Points with missing values are not allowed.", call. = TRUE)
  }
  # linear inequalities
  V <- makeV(points)
  H <- scdd(V)[["output"]]
  A <- - H[, -c(1L, 2L)]
  b <- H[, 2L]
  # problem variables
  Bvar <- Variable(2L, 2L, symmetric = TRUE)
  dvar <- Variable(2L)
  # objective
  objective <- Minimize(-log_det(Bvar))
  #constraints
  constraints <- list()
  for(i in 1L:nrow(A)) {
    constraints <- append(
      constraints, list(norm2(Bvar %*% A[i, ]) + sum(A[i, ]*dvar) <= b[i])
    )
  }
  # solve the problem
  program <- Problem(objective, constraints)
  solution <- psolve(program, solver = "SCS", verbose = verbose)
  status <- solution[["status"]]
  if(status != "optimal") {
    warning("Non-optimal solution.")
  }
  # get solutions
  B <- solution$getValue(Bvar)
  d <- c(solution$getValue(dvar))
  # get ellipse
  aff <- Affine$new(B, d)
  unitcircle <- CircleOA(c(0, 0), c(1, 0))
  ell <- aff$transformEllipse(unitcircle)
  attr(ell, "status") <- status
  ell
}

Try the PlaneGeometry package in your browser

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

PlaneGeometry documentation built on Aug. 10, 2023, 1:09 a.m.