1

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?

0__
  • 66,707
  • 21
  • 171
  • 266

1 Answers1

1

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
  }
}
0__
  • 66,707
  • 21
  • 171
  • 266