I would like to add custom forces to a force-directed-layout in Prefuse. Specifically I would like to make a) edges between specific nodes very rigid, b) introduce directed edges such that the source vertex tends to move above the target vertex.
Any clues how to proceed?
One can sub-class the standard SpringForce
and add further constraints. The following works pretty well:
import prefuse.util.force.{Spring, SpringForce}
object MySpringForce {
private final val pi = math.Pi.toFloat
private final val piH = (math.Pi/2).toFloat
private final val eps = (1 * math.Pi/180).toFloat
}
class MySpringForce extends SpringForce {
import MySpringForce._
private val TORQUE = params.length
private val DISTANCE = TORQUE + 1
params = params ++ Array[Float](5e-5f, -1f)
minValues = minValues ++ Array[Float](0f , -1f)
maxValues = maxValues ++ Array[Float](1e-3f, 500f)
override def getParameterNames: Array[String] =
super.getParameterNames ++ Array("Torque", "Limit")
private def angleBetween(a: Float, b: Float): Float = {
val d = b - a
math.atan2(math.sin(d), math.cos(d)).toFloat
}
override def getForce(s: Spring): Unit = {
val item1 = s.item1
val item2 = s.item2
val length = if (s.length < 0)
params(SpringForce.SPRING_LENGTH) else s.length
val x1 = item1.location(0)
val y1 = item1.location(1)
val x2 = item2.location(0)
val y2 = item2.location(1)
var dx = x2 - x1
var dy = y2 - y1
val r0 = math.sqrt(dx * dx + dy * dy).toFloat
val r1 = if (r0 == 0.0) {
dx = (math.random.toFloat - 0.5f) / 50.0f
dy = (math.random.toFloat - 0.5f) / 50.0f
math.sqrt(dx * dx + dy * dy).toFloat
} else r0
val dist = params(DISTANCE)
val r = if (dist < 0) r1 else math.min(dist, r1)
val d = r - length
val coeff = (if (s.coeff < 0)
params(SpringForce.SPRING_COEFF) else s.coeff) * d / r
item1.force(0) += coeff * dx
item1.force(1) += coeff * dy
item2.force(0) += -coeff * dx
item2.force(1) += -coeff * dy
val ang = math.atan2(dy, dx).toFloat
val da = angleBetween(ang, -piH)
if (math.abs(da) <= eps) return
val af = da / pi * params(TORQUE)
val rH = r/2
val cx = (x1 + x2) / 2
val cy = (y1 + y2) / 2
val cos = math.cos(ang + af).toFloat * rH
val sin = math.sin(ang + af).toFloat * rH
val x1t = cx - cos
val y1t = cy - sin
val x2t = cx + cos
val y2t = cy + sin
item1.force(0) += x1t - x1
item1.force(1) += y1t - y1
item2.force(0) += x2t - x2
item2.force(1) += y2t - y2
}
}