在 Prefuse 的 ForceDirectedLayout 中实现自定义力

Implement a custom force in Prefuse's ForceDirectedLayout

我想将自定义力添加到 Prefuse 中的力导向布局。具体来说,我想让 a) 特定节点之间的边非常严格,b) 引入有向边,使源顶点倾向于移动到目标顶点上方。

任何线索如何进行?

可以子class 标准SpringForce 并添加更多约束。以下效果很好:

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
  }
}