Charges-and-fields-scala


package edu.colorado.phet.chargesandfields

import _root_.scala.collection.mutable.ArrayBuffer
import common.phetcommon.application.Module
import common.phetcommon.model.BaseModel
import common.phetcommon.view.controls.valuecontrol.LinearValueControl
import common.phetcommon.view.graphics.transforms.ModelViewTransform2D
import common.phetcommon.view.util.PhetFont
import common.phetcommon.view.VerticalLayoutPanel
import common.piccolophet.event.CursorHandler
import common.piccolophet.PhetPCanvas
import java.awt.geom.Rectangle2D
import java.awt.{Rectangle, Dimension, Color}
import edu.colorado.phet.scalacommon.Predef._
import javax.swing.event.{ChangeListener, ChangeEvent}
import javax.swing.{JButton, BoxLayout, JPanel}
import scalacommon.math.Vector2D
import scalacommon.util.Observable
import umd.cs.piccolo.event.{PBasicInputEventHandler, PInputEvent}
import umd.cs.piccolo.nodes.PText
import umd.cs.piccolo.PNode
import umd.cs.piccolo.util.PBounds
import scalacommon.{ScalaApplicationLauncher, ScalaClock}
import umd.cs.piccolo.util.PPaintContext
import javax.swing.JComponent
import _root_.edu.colorado.phet.common.piccolophet.PhetPCanvas.TransformStrategy
import java.awt.geom.{AffineTransform, Rectangle2D}

class DefaultScreenCanvas(modelWidth: Double, modelHeight: Double) extends PhetPCanvas(new Dimension(1024, 768)) {
  val worldNode = new PNode
  addScreenChild(worldNode)
  def addNode(node: PNode) = worldNode.addChild(node)

  def addNode(index: Int, node: PNode) = worldNode.addChild(index, node)
}

//state class is useful if we want to record/playback
class ChargeState(_position: Vector2D, _velocity: Vector2D) {
  val position = _position
  val velocity = _velocity

  def translate(delta: Vector2D) = new ChargeState(position + delta, velocity)
}

object MyRandom extends scala.util.Random

class Charge(_positive: Boolean) extends Observable {
  val positive = _positive
  var state = new ChargeState(new Vector2D(200, 200), new Vector2D(MyRandom.nextDouble() * 30 + 10, MyRandom.nextDouble() * 30 + 10))

  def translate(delta: Vector2D) = {
    state = state.translate(delta)
    notifyListeners()
  }

  def position = state.position

  def velocity = state.velocity
}

class ChargesAndFieldsModel {
  val charges = new ArrayBuffer[Charge]
  charges += new Charge(true)
  val chargeAddedListeners = new ArrayBuffer[Charge => Unit]
  val chargeRemovedListeners = new ArrayBuffer[Charge => Unit]
  val listeners = new ArrayBuffer[() => Unit]
  var cellSpacing = 10.0

  def setCellSpacing(spacing: Double) = {
    cellSpacing = spacing
    listeners.foreach(_())
  }

  def addCharge() = {
    val charge = new Charge(MyRandom.nextBoolean())
    charge.translate(new Vector2D(MyRandom.nextDouble() * 500, MyRandom.nextDouble * 500))
    charges += charge
    chargeAddedListeners.foreach(_(charge))
  }

  def getV(x: Double, y: Double): Double = {
    var volts = 0.0;
    for (b <- charges) {
      volts = volts + getV(x, y, b)
    }
    volts
  }

  def getV(x: Double, y: Double, b: Charge): Double = {
    val dx = b.position.x - x
    val dy = b.position.y - y
    val dist = Math.sqrt(dx * dx + dy * dy)
    if (dist > 0)
      1 / dist * (if (b.positive) 1 else -1)
    else
      10000 * (if (b.positive) 1 else -1)
  }
}

class ChargeNode(b: Charge) extends PText(if (b.positive) "+" else "-") {
  addInputEventListener(new CursorHandler)
  setFont(new PhetFont(34, true))
  defineInvokeAndPass(b.addListenerByName){
    setOffset(b.position)
  }
  addInputEventListener(new PBasicInputEventHandler {
    override def mouseDragged(event: PInputEvent) = {
      val dim = event.getDeltaRelativeTo(ChargeNode.this.getParent)
      b.translate(new Vector2D(dim.width, dim.height))
    }
  })
}

class LatticeNode(model: ChargesAndFieldsModel, _canvas: JComponent) extends PNode {
  val canvas = _canvas
  val bounds: PBounds = new PBounds(0, 0, 1024, 768)
  model.charges(0).addListenerByName(repaint)
  model.chargeAddedListeners += (b => {
    b.addListenerByName(repaint)
    repaint
  })
  model.listeners += (() => repaint)

  protected override def paint(paintContext: PPaintContext) = {
    val step = model.cellSpacing.toInt;
    val nx = (canvas.getWidth / step).toInt
    val ny = (canvas.getHeight / step).toInt
    for (x <- 0 to nx;
         y <- 0 to ny) {
      val ox = x * step
      val oy = y * step
      val volts: Double = model.getV(ox + step / 2, oy + step / 2)
      def voltsToColor(sumV: Double): Color = {
        //set color associated with voltage
        val maxV = 0.01
        def scale(a: Double) = Math.min(Math.abs(a) / maxV, 1).toFloat
        if (sumV > 0) {
          val green = 1 - scale(sumV)
          val blue = green
          new Color(1f, green, blue)
        } else {
          val red = 1 - scale(sumV)
          val green = red
          new Color(red, green, 1f)
        }
      }
      val color = voltsToColor(volts)
      paintContext.getGraphics.setColor(color)
      paintContext.getGraphics.fillRect(ox, oy, step, step)
    }
  }

  override def getFullBoundsReference = {
    new PBounds(0, 0, canvas.getWidth, canvas.getHeight)
  }
}

class ChargesAndFieldsCanvas(model: ChargesAndFieldsModel) extends DefaultScreenCanvas(20, 20) {
  val chargeNode = new ChargeNode(model.charges(0))
  addNode(new LatticeNode(model, this))
  addNode(chargeNode)

  model.chargeAddedListeners += (b => addNode(new ChargeNode(b)))
}

class ChargesAndFieldsControlPanel(model: ChargesAndFieldsModel) extends JPanel {
  setLayout(new BoxLayout(this, BoxLayout.Y_AXIS))
  val button = new JButton("Add Charge")
  button.addActionListenerByName(model.addCharge())
  add(button)

  val button20 = new JButton("Add 20")
  button20.addActionListenerByName(for (i <- 1 to 20) model.addCharge())
  add(button20)

  val slider = new LinearValueControl(1, 100, model.cellSpacing, "cell size", "0", "")
  slider.setTextFieldColumns(4)
  slider.addChangeListener(new ChangeListener {
    def stateChanged(e: ChangeEvent) = model.setCellSpacing(slider.getValue)
  })
  add(slider)
}

class ChargesAndFieldsModule(clock: ScalaClock) extends Module("ChargesAndFields", clock) {
  val model = new ChargesAndFieldsModel
  setSimulationPanel(new ChargesAndFieldsCanvas(model))
  setControlPanel(new ChargesAndFieldsControlPanel(model))
}

object ChargesAndFieldsApplication {
  def main(args: Array[String]) = {
    ScalaApplicationLauncher.launchApplication(args, "charges-and-fields-scala", "charges-and-fields-scala", () => new ChargesAndFieldsModule(new ScalaClock(30, 30 / 1000.0)))
  }
}