blob: 9e525659f5b892ecf58c63741731e91a72810b16 [file] [log] [blame]
# File src/library/grid/R/curve.R
# Part of the R package, https://www.R-project.org
#
# Copyright (C) 1995-2012 The R Core Team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A copy of the GNU General Public License is available at
# https://www.R-project.org/Licenses/
###############################
# CURVE primitive
###############################
calcOrigin <- function(x1, y1, x2, y2, origin, hand) {
# Positive origin means origin to the "right"
# Negative origin means origin to the "left"
xm <- (x1 + x2)/2
ym <- (y1 + y2)/2
dx <- x2 - x1
dy <- y2 - y1
slope <- dy/dx
oslope <- -1/slope
# The origin is a point somewhere along the line between
# the end points, rotated by 90 (or -90) degrees
# Two special cases:
# If slope is non-finite then the end points lie on a vertical line, so
# the origin lies along a horizontal line (oslope = 0)
# If oslope is non-finite then the end points lie on a horizontal line,
# so the origin lies along a vertical line (oslope = Inf)
tmpox <- ifelse(!is.finite(slope),
xm,
ifelse(!is.finite(oslope),
xm + origin*(x2 - x1)/2,
xm + origin*(x2 - x1)/2))
tmpoy <- ifelse(!is.finite(slope),
ym + origin*(y2 - y1)/2,
ifelse(!is.finite(oslope),
ym,
ym + origin*(y2 - y1)/2))
# ALWAYS rotate by -90 about midpoint between end points
# Actually no need for "hand" because "origin" also
# encodes direction
# sintheta <- switch(hand, left=-1, right=1)
sintheta <- -1
ox <- xm - (tmpoy - ym)*sintheta
oy <- ym + (tmpox - xm)*sintheta
list(x=ox, y=oy)
}
# Given ncp*ncurve vector of values, ncurve vector of start values,
# ncurve vector of end values, ncurve vector of end logicals,
# combine start or end values with original values based on logicals
interleave <- function(ncp, ncurve, val, sval, eval, e) {
sval <- rep(sval, length.out=ncurve)
eval <- rep(eval, length.out=ncurve)
result <- matrix(NA, ncol=ncurve, nrow=ncp+1)
m <- matrix(val, ncol=ncurve)
for (i in 1L:ncurve) {
if (e[i])
result[,i] <- c(m[,i], eval[i])
else
result[,i] <- c(sval[i], m[,i])
}
as.numeric(result)
}
# Calculate a "square" set of end points to calculate control points from
# NOTE: end points may be vector
calcSquareControlPoints <- function(x1, y1, x2, y2,
curvature, angle, ncp,
debug=FALSE) {
dx <- x2 - x1
dy <- y2 - y1
slope <- dy/dx
# FIXME: There MUST be a more compact way of calculating the
# new end point!
end <- (slope > 1 |
(slope < 0 & slope > -1))
if (curvature < 0)
end <- !end
startx <- ifelse(end,
x1,
ifelse(abs(slope) > 1, x2 - dx, x2 - sign(slope)*dy))
starty <- ifelse(end,
y1,
ifelse(abs(slope) > 1, y2 - sign(slope)*dx, y2 - dy))
endx <- ifelse(end,
ifelse(abs(slope) > 1, x1 + dx, x1 + sign(slope)*dy),
x2)
endy <- ifelse(end,
ifelse(abs(slope) > 1, y1 + sign(slope)*dx, y1 + dy),
y2)
cps <- calcControlPoints(startx, starty, endx, endy,
curvature, angle, ncp,
debug)
# Intereave control points and extra "square" control points
ncurve <- length(x1)
cps$x <- interleave(ncp, ncurve, cps$x, startx, endx, end)
cps$y <- interleave(ncp, ncurve, cps$y, starty, endy, end)
list(x=cps$x, y=cps$y, end=end)
}
# Find origin of rotation
# Rotate around that origin
calcControlPoints <- function(x1, y1, x2, y2, curvature, angle, ncp,
debug=FALSE) {
# Negative curvature means curve to the left
# Positive curvature means curve to the right
# Special case curvature = 0 (straight line) has been handled
xm <- (x1 + x2)/2
ym <- (y1 + y2)/2
dx <- x2 - x1
dy <- y2 - y1
slope <- dy/dx
# Calculate "corner" of region to produce control points in
# (depends on 'angle', which MUST lie between 0 and 180)
# Find by rotating start point by angle around mid point
if (is.null(angle)) {
# Calculate angle automatically
angle <- ifelse(slope < 0,
2*atan(abs(slope)),
2*atan(1/slope))
} else {
angle <- angle/180*pi
}
sina <- sin(angle)
cosa <- cos(angle)
# FIXME: special case of vertical or horizontal line ?
cornerx <- xm + (x1 - xm)*cosa - (y1 - ym)*sina
cornery <- ym + (y1 - ym)*cosa + (x1 - xm)*sina
# Debugging
if (debug) {
grid.points(cornerx, cornery, default.units="inches",
pch=16, size=unit(3, "mm"),
gp=gpar(col="grey"))
}
# Calculate angle to rotate region by to align it with x/y axes
beta <- -atan((cornery - y1)/(cornerx - x1))
sinb <- sin(beta)
cosb <- cos(beta)
# Rotate end point about start point to align region with x/y axes
newx2 <- x1 + dx*cosb - dy*sinb
newy2 <- y1 + dy*cosb + dx*sinb
# Calculate x-scale factor to make region "square"
# FIXME: special case of vertical or horizontal line ?
scalex <- (newy2 - y1)/(newx2 - x1)
# Scale end points to make region "square"
newx1 <- x1*scalex
newx2 <- newx2*scalex
# Calculate the origin in the "square" region
# (for rotating start point to produce control points)
# (depends on 'curvature')
# 'origin' calculated from 'curvature'
ratio <- 2*(sin(atan(curvature))^2)
origin <- curvature - curvature/ratio
# 'hand' also calculated from 'curvature'
if (curvature > 0)
hand <- "right"
else
hand <- "left"
oxy <- calcOrigin(newx1, y1, newx2, newy2, origin, hand)
ox <- oxy$x
oy <- oxy$y
# Calculate control points
# Direction of rotation depends on 'hand'
dir <- switch(hand,
left=-1,
right=1)
# Angle of rotation depends on location of origin
maxtheta <- pi + sign(origin*dir)*2*atan(abs(origin))
theta <- seq(0, dir*maxtheta,
dir*maxtheta/(ncp + 1))[c(-1, -(ncp + 2))]
costheta <- cos(theta)
sintheta <- sin(theta)
# May have BOTH multiple end points AND multiple
# control points to generate (per set of end points)
# Generate consecutive sets of control points by performing
# matrix multiplication
cpx <- ox + ((newx1 - ox) %*% t(costheta)) -
((y1 - oy) %*% t(sintheta))
cpy <- oy + ((y1 - oy) %*% t(costheta)) +
((newx1 - ox) %*% t(sintheta))
# Reverse transformations (scaling and rotation) to
# produce control points in the original space
cpx <- cpx/scalex
sinnb <- sin(-beta)
cosnb <- cos(-beta)
finalcpx <- x1 + (cpx - x1)*cosnb - (cpy - y1)*sinnb
finalcpy <- y1 + (cpy - y1)*cosnb + (cpx - x1)*sinnb
# Debugging
if (debug) {
ox <- ox/scalex
fox <- x1 + (ox - x1)*cosnb - (oy - y1)*sinnb
foy <- y1 + (oy - y1)*cosnb + (ox - x1)*sinnb
grid.points(fox, foy, default.units="inches",
pch=16, size=unit(1, "mm"),
gp=gpar(col="grey"))
grid.circle(fox, foy, sqrt((ox - x1)^2 + (oy - y1)^2),
default.units="inches",
gp=gpar(col="grey"))
}
list(x=as.numeric(t(finalcpx)), y=as.numeric(t(finalcpy)))
}
# Debugging
cbDiagram <- function(x1, y1, x2, y2, cps) {
grid.segments(x1, y1, x2, y2,
gp=gpar(col="grey"),
default.units="inches")
grid.points(x1, y1, pch=16, size=unit(1, "mm"),
gp=gpar(col="green"),
default.units="inches")
grid.points(x2, y2, pch=16, size=unit(1, "mm"),
gp=gpar(col="red"),
default.units="inches")
grid.points(cps$x, cps$y, pch=16, size=unit(1, "mm"),
default.units="inches",
gp=gpar(col="blue"))
}
straightCurve <- function(x1, y1, x2, y2, arrow, debug) {
if (debug) {
xm <- (x1 + x2)/2
ym <- (y1 + y2)/2
cbDiagram(x1, y1, x2, y2, list(x=xm, y=ym))
}
segmentsGrob(x1, y1, x2, y2,
default.units="inches",
arrow=arrow, name="segment")
}
# Return a gTree (even if it only has one grob as a child)
# because that is the only way to get more than one child
# to draw
calcCurveGrob <- function(x, debug) {
x1 <- x$x1
x2 <- x$x2
y1 <- x$y1
y2 <- x$y2
curvature <- x$curvature
angle <- x$angle
ncp <- x$ncp
shape <- x$shape
square <- x$square
squareShape <- x$squareShape
inflect <- x$inflect
arrow <- x$arrow
open <- x$open
# Calculate a set of control points based on:
# 'curvature', ' angle', and 'ncp',
# and the start and end point locations.
# The origin is a point along the perpendicular bisector
# of the line between the end points.
# The control points are found by rotating the end points
# about the origin.
# Do everything in inches to make things easier.
# Because this is within a makeContent() method,
# the conversions will not be an
# issue (in terms of device resizes).
x1 <- convertX(x1, "inches", valueOnly=TRUE)
y1 <- convertY(y1, "inches", valueOnly=TRUE)
x2 <- convertX(x2, "inches", valueOnly=TRUE)
y2 <- convertY(y2, "inches", valueOnly=TRUE)
# Outlaw identical end points
if (any(x1 == x2 & y1 == y2))
stop("end points must not be identical")
# Rep locations to allow multiple curves from single call
maxn <- max(length(x1),
length(y1),
length(x2),
length(y2))
x1 <- rep(x1, length.out=maxn)
y1 <- rep(y1, length.out=maxn)
x2 <- rep(x2, length.out=maxn)
y2 <- rep(y2, length.out=maxn)
if (!is.null(arrow))
arrow <- rep(arrow, length.out=maxn)
if (curvature == 0) {
children <- gList(straightCurve(x1, y1, x2, y2, arrow, debug))
} else {
# Treat any angle less than 1 or greater than 179 degrees
# as a straight line
# Takes care of some nasty limit effects as well as simplifying
# things
if (angle < 1 || angle > 179) {
children <- gList(straightCurve(x1, y1, x2, y2, arrow, debug))
} else {
# Handle 'square' vertical and horizontal lines
# separately
if (square && any(x1 == x2 | y1 == y2)) {
subset <- x1 == x2 | y1 == y2
straightGrob <- straightCurve(x1[subset], y1[subset],
x2[subset], y2[subset],
arrow, debug)
# Remove these from the curves to draw
x1 <- x1[!subset]
x2 <- x2[!subset]
y1 <- y1[!subset]
y2 <- y2[!subset]
if (!is.null(arrow))
arrow <- arrow[!subset]
} else {
straightGrob <- NULL
}
ncurve <- length(x1)
# If nothing to draw, we're done
if (ncurve == 0) {
children <- gList(straightGrob)
} else {
if (inflect) {
xm <- (x1 + x2)/2
ym <- (y1 + y2)/2
shape1 <- rep(rep(shape, length.out=ncp), ncurve)
shape2 <- rev(shape1)
if (square) {
# If 'square' then add an extra control point
cps1 <- calcSquareControlPoints(x1, y1, xm, ym,
curvature, angle,
ncp,
debug=debug)
cps2 <- calcSquareControlPoints(xm, ym, x2, y2,
-curvature, angle,
ncp,
debug=debug)
shape1 <- interleave(ncp, ncurve, shape1,
squareShape, squareShape,
cps1$end)
shape2 <- interleave(ncp, ncurve, shape2,
squareShape, squareShape,
cps2$end)
ncp <- ncp + 1
} else {
cps1 <- calcControlPoints(x1, y1, xm, ym,
curvature, angle, ncp,
debug=debug)
cps2 <- calcControlPoints(xm, ym, x2, y2,
-curvature, angle, ncp,
debug=debug)
}
if (debug) {
cbDiagram(x1, y1, xm, ym, cps1)
cbDiagram(xm, ym, x2, y2, cps2)
}
idset <- 1L:ncurve
splineGrob <-
xsplineGrob(c(x1, cps1$x, xm, cps2$x, x2),
c(y1, cps1$y, ym, cps2$y, y2),
id=c(idset, rep(idset, each=ncp),
idset, rep(idset, each=ncp),
idset),
default.units="inches",
shape=c(rep(0, ncurve), shape1,
rep(0, ncurve), shape2,
rep(0, ncurve)),
arrow=arrow, open=open,
name="xspline")
if (is.null(straightGrob)) {
children <- gList(splineGrob)
} else {
children <- gList(straightGrob, splineGrob)
}
} else {
shape <- rep(rep(shape, length.out=ncp), ncurve)
if (square) {
# If 'square' then add an extra control point
cps <- calcSquareControlPoints(x1, y1, x2, y2,
curvature, angle,
ncp,
debug=debug)
shape <- interleave(ncp, ncurve, shape,
squareShape, squareShape,
cps$end)
ncp <- ncp + 1
} else {
cps <- calcControlPoints(x1, y1, x2, y2,
curvature, angle, ncp,
debug=debug)
}
if (debug) {
cbDiagram(x1, y1, x2, y2, cps)
}
idset <- 1L:ncurve
splineGrob <- xsplineGrob(c(x1, cps$x, x2),
c(y1, cps$y, y2),
id=c(idset,
rep(idset, each=ncp), idset),
default.units="inches",
shape=c(rep(0, ncurve), shape,
rep(0, ncurve)),
arrow=arrow, open=open,
name="xspline")
if (is.null(straightGrob)) {
children <- gList(splineGrob)
} else {
children <- gList(straightGrob, splineGrob)
}
}
}
}
}
gTree(children=children,
name=x$name, gp=x$gp, vp=x$vp)
}
validDetails.curve <- function(x) {
if ((!is.unit(x$x1) || !is.unit(x$y1)) ||
(!is.unit(x$x2) || !is.unit(x$y2)))
stop("'x1', 'y1', 'x2', and 'y2' must be units")
x$curvature <- as.numeric(x$curvature)
x$angle <- x$angle %% 180
x$ncp <- as.integer(x$ncp)
if (x$shape < -1 || x$shape > 1)
stop("'shape' must be between -1 and 1")
x$square <- as.logical(x$square)
if (x$squareShape < -1 || x$squareShape > 1)
stop("'squareShape' must be between -1 and 1")
x$inflect <- as.logical(x$inflect)
if (!is.null(x$arrow) && !inherits(x$arrow, "arrow"))
stop("'arrow' must be an arrow object or NULL")
x$open <- as.logical(x$open)
x
}
makeContent.curve <- function(x) {
calcCurveGrob(x, x$debug)
}
xDetails.curve <- function(x, theta) {
cg <- calcCurveGrob(x, FALSE)
# Could do better here
# (result for more than 1 child is basically to give up)
if (length(cg$children) == 1)
xDetails(cg$children[[1]], theta)
else
xDetails(cg, theta)
}
yDetails.curve <- function(x, theta) {
cg <- calcCurveGrob(x, FALSE)
if (length(cg$children) == 1)
yDetails(cg$children[[1]], theta)
else
yDetails(cg, theta)
}
widthDetails.curve <- function(x) {
cg <- calcCurveGrob(x, FALSE)
if (length(cg$children) == 1)
widthDetails(cg$children[[1]])
else
widthDetails(cg)
}
heightDetails.curve <- function(x) {
cg <- calcCurveGrob(x, FALSE)
if (length(cg$children) == 1)
heightDetails(cg$children[[1]])
else
heightDetails(cg)
}
curveGrob <- function(x1, y1, x2, y2, default.units="npc",
curvature=1, angle=90, ncp=1,
shape=0.5, square=TRUE, squareShape=1,
inflect=FALSE, arrow=NULL, open=TRUE,
debug=FALSE,
name=NULL, gp=gpar(), vp=NULL) {
# FIXME: add arg checking
# FIXME: angle MUST be between 0 and 180
if (!is.unit(x1))
x1 <- unit(x1, default.units)
if (!is.unit(y1))
y1 <- unit(y1, default.units)
if (!is.unit(x2))
x2 <- unit(x2, default.units)
if (!is.unit(y2))
y2 <- unit(y2, default.units)
gTree(x1=x1, y1=y1, x2=x2, y2=y2,
curvature=curvature, angle=angle, ncp=ncp,
shape=shape, square=square, squareShape=squareShape,
inflect=inflect, arrow=arrow, open=open, debug=debug,
name=name, gp=gp, vp=vp,
cl="curve")
}
grid.curve <- function(...) {
grid.draw(curveGrob(...))
}
# Calculate the curvature to use if you want to produce control
# points lying along the arc of a circle that spans theta degrees
# (Use ncp=8 and shape=-1 to actually produce such an arc)
arcCurvature <- function(theta) {
# Avoid limiting cases (just draw a straight line)
if (theta < 1 || theta > 359)
return(0)
angle <- 0.5*theta/180*pi
1/sin(angle) - 1/tan(angle)
}