diff options
author | Jakob Odersky <jakob@odersky.com> | 2016-02-24 20:29:30 -0800 |
---|---|---|
committer | Jakob Odersky <jakob@odersky.com> | 2016-02-24 20:31:14 -0800 |
commit | a41de68066007852d7d3dbf019d75b4caf7463ad (patch) | |
tree | b4446408291c9f179e1c270a561523023ac6a105 /mavigator-cockpit/src/main/scala/mavigator/dashboard | |
parent | 245faaf1e2ff4d0fbda292dbb35f4b49426d4380 (diff) | |
download | mavigator-a41de68066007852d7d3dbf019d75b4caf7463ad.tar.gz mavigator-a41de68066007852d7d3dbf019d75b4caf7463ad.tar.bz2 mavigator-a41de68066007852d7d3dbf019d75b4caf7463ad.zip |
Major refactorings
Diffstat (limited to 'mavigator-cockpit/src/main/scala/mavigator/dashboard')
15 files changed, 733 insertions, 0 deletions
diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala new file mode 100644 index 0000000..f3a111a --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala @@ -0,0 +1,23 @@ +package mavigator.dashboard + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSExport + +import org.scalajs.dom.html + +import mavigator.dashboard.ui.Layout +import mavigator.util.Environment +import mavigator.util.Application + +import scalatags.JsDom.all._ + +@JSExport("mavigator_dashboard_Main") +object Main extends Application { + + override def main(args: Map[String, String])(implicit env: Environment): Unit = { + val socket = new MavlinkSocket(args("socketUrl"), args("remoteSystemId").toInt) + val layout = new Layout(socket) + + env.root.appendChild(layout.element) + } +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala new file mode 100644 index 0000000..7f4ffdc --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala @@ -0,0 +1,70 @@ +package mavigator.dashboard + +import scala.scalajs.js +import scala.scalajs.js.Any.fromFunction1 +import org.mavlink.Packet +import org.mavlink.Parser +import org.mavlink.Parser.Errors._ +import org.mavlink.messages.Message +import org.scalajs.dom +import scala.concurrent.duration._ +import rx._ +import rx.ops._ +import scala.concurrent.ExecutionContext.Implicits.global + +class MavlinkSocket(url: String, val remoteSystemId: Int) { + implicit val scheduler = new DomScheduler + + lazy val packet: Var[Packet] = Var(Packet.empty) + lazy val message: Rx[Message] = packet.map{p => + Message.unpack(p.messageId, p.payload) + } + + object stats { + private val DebounceTime = 1.seconds + + private[MavlinkSocket] val _crcErrors = Var(0) + private[MavlinkSocket] val _overflows = Var(0) + private[MavlinkSocket] val _wrongIds = Var(0) + private[MavlinkSocket] val _packets = Var(0) + + val crcErrors = _crcErrors.debounce(DebounceTime) + val overflows = _overflows.debounce(DebounceTime) + val wrongIds = _wrongIds.debounce(DebounceTime) + val packets = _packets.debounce(DebounceTime) + val open = Var(false) + } + + private val parser = new Parser( + { + case pckt@Packet(seq, `remoteSystemId`, compId, msgId, payload) => + packet() = pckt + stats._packets() += 1 + case _ => + stats._wrongIds() += 1 + }, + { + case CrcError => stats._crcErrors() += 1 + case OverflowError => stats._overflows() += 1 + }) + + private val connection = new dom.WebSocket(url) + + connection.binaryType = "arraybuffer" + + connection.onopen = (e: dom.Event) => { + stats.open() = true + } + connection.onmessage = (e: dom.MessageEvent) => { + val buffer = e.data.asInstanceOf[js.typedarray.ArrayBuffer] + val view = new js.typedarray.DataView(buffer) + + for (i <- 0 until view.byteLength) { + parser.push(view.getInt8(i)) + } + } + connection.onclose = (e: dom.CloseEvent) => { + stats.open() = false + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala new file mode 100644 index 0000000..2637ddf --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala @@ -0,0 +1,70 @@ +package mavigator.dashboard + +import scala.language.implicitConversions +import scala.util.Failure +import scala.util.Success + +import org.scalajs.dom.html + +import rx.Obs +import rx.Rx +import rx.Rx +import rx.Var +import rx.Var +import scalatags.JsDom.all.Frag +import scalatags.JsDom.all.HtmlTag +import scalatags.JsDom.all.backgroundColor +import scalatags.JsDom.all.bindNode +import scalatags.JsDom.all.span +import scalatags.JsDom.all.stringFrag +import scalatags.JsDom.all.stringStyle + +package object rxutil { + + /** Rx, implicitly enhanced with additional methods. */ + implicit class RichRx(val rx: Rx[_]) extends AnyVal { + + /** + * Builds a new Rx by applying a partial function to all values of + * this Rx on which the function is defined. + * @param initial initial value of the returned Rx + * @param pf the partial function which filters and maps this Rx + * @return a new Rx resulting from applying the given partial + * function pf to each value on which it is defined and collecting + * the result + */ + def collect[B](initial: B)(pf: PartialFunction[Any, B]): Rx[B] = { + val result: Var[B] = Var(initial) + Obs(rx, skipInitial = true) { + if (pf.isDefinedAt(rx())) { + result() = pf(rx()) + } + } + result + } + + } + + /** + * Copied from https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/Framework.scala + * + * Sticks some Rx into a Scalatags fragment, which means hooking up an Obs + * to propagate changes into the DOM via the element's ID. Monkey-patches + * the Obs onto the element itself so we have a reference to kill it when + * the element leaves the DOM (e.g. it gets deleted). + */ + implicit def rxMod[T <: html.Element](r: Rx[HtmlTag]): Frag = { + def rSafe = r.toTry match { + case Success(v) => v.render + case Failure(e) => span(e.toString, backgroundColor := "red").render + } + var last = rSafe + Obs(r, skipInitial = true) { + val newLast = rSafe + last.parentElement.replaceChild(newLast, last) + last = newLast + } + bindNode(last) + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala new file mode 100644 index 0000000..18c5c27 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala @@ -0,0 +1,47 @@ +package mavigator.dashboard.ui + +import mavigator.util.Environment +import mavigator.dashboard.ui.instruments._ + +import scalatags.JsDom.all._ +import rx._ + +class Hud(attitude: Rx[(Double, Double)])(implicit env: Environment) { + + private def overlay(name: String, z: Int, thinnerThanWide: Boolean = false) = { + val direction = if (thinnerThanWide) "row" else "column" + div( + style:= + "position: absolute; left: 0; right: 0; top: 0; bottom: 0;" + + s"display: flex; align-content: center; align-items: stretch; flex-direction: $direction;"+ + s"z-index: $z;" + )( + `object`(`type`:="image/svg+xml", "data".attr:=env.asset("images/hud/" + name + ".svg"), style := "flex: 1 1 100%;") + ) + } + + object Horizon extends SvgInstrument[(Double, Double)] { + import SvgInstrument._ + + val value = attitude + + lazy val element = `object`(`type`:="image/svg+xml", "data".attr:=env.asset("images/hud/horizon.svg"), style:="flex: 1 1 100%;").render + lazy val horizon = part("horizon") + lazy val moveable = Seq(horizon) + + protected def update(pitchRoll: (Double, Double)) = { + rotate(horizon, pitchRoll._2) + //translate(horizon, 0, (pitchRoll._1 * 180 / math.Pi).toInt) // 1deg === 1px + } + } + + val element = div( + style:= + "position: absolute; left: 0; right: 0; top: 0; bottom: 0;" + + "display: flex; align-content: stretch; align-items: stretch; flex-direction: row;"+ + "z-index: 0;" + )( + Horizon.element + ) +} + diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala new file mode 100644 index 0000000..902cb9c --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala @@ -0,0 +1,266 @@ +package mavigator.dashboard.ui + +import rx._ +import scalatags.JsDom.all._ +import mavigator.util.Environment +import mavigator.dashboard.MavlinkSocket +import mavigator.dashboard.ui.instruments._ +import org.mavlink.messages._ +import mavigator.dashboard.rxutil._ + +class Layout(socket: MavlinkSocket)(implicit env: Environment) { + + private def panel(contents: Frag*) = div(`class` := "d-panel")(contents: _*) + + private def mode(name: String, kind: String, on: Boolean = false) = div(`class` := s"mode $kind ${if (!on) "off"}")(name) + + val modes = div( + mode("MANUAL", "warning", true), + mode("STABILIZED", "info", true), + mode("GUIDED", "success", true), + mode("AUTO", "success", true)) + + val infos = div( + mode("BAY", "info"), + mode("RECOVERY", "danger"), + mode("NOGPS", "warning", true), + mode("OVERLOAD", "danger", true), + mode("BATTERY", "danger", false), + mode("LINK", "danger", true), + mode("SOCKET", "danger", true), + div(style := "float: right")(mode("CRITICAL", "danger", true))) + + val map = iframe( + "frameborder".attr := "0", + "scrolling".attr := "no", + "marginheight".attr := "0", + "marginwidth".attr := "0", + src := "http://www.openstreetmap.org/export/embed.html?bbox=6.5611016750335684%2C46.51718501017836%2C6.570038795471191%2C46.520577350893525&layer=mapnik") + + val feed = div(style := "width: 100%; height: 100%; color: #ffffff; background-color: #c2c2c2; text-align: center;") + + val altimeter = new Altimeter( + Var(0.0) + ) + val horizon = new Horizon(socket.message.collect((0.0, 0.0)) { + case att: Attitude => (att.pitch, att.roll) + }) + val compass = new Compass(socket.message.collect(0.0) { + case att: Attitude => att.yaw + }) + val motor0 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { + case s: ServoOutputRaw => 100 * (s.servo1Raw - 1000) / 1000 + }) + val motor1 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { + case s: ServoOutputRaw => 100 * (s.servo2Raw - 1000) / 1000 + }) + val motor2 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { + case s: ServoOutputRaw => 100 * (s.servo3Raw - 1000) / 1000 + }) + val motor3 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { + case s: ServoOutputRaw => 100 * (s.servo4Raw - 1000) / 1000 + }) + val powerDistribution = new Distribution( + socket.message.collect((0.0, 0.0, 0.0, 0.0)) { + case s: ServoOutputRaw => + ( + 1.0 * (s.servo1Raw - 1000) / 1000, + 1.0 * (s.servo2Raw - 1000) / 1000, + 1.0 * (s.servo3Raw - 1000) / 1000, + 1.0 * (s.servo4Raw - 1000) / 1000 + ) + } + ) + val batteryLevel = new Bar( + Var(0.0) + ) + + val top = header( + div("Flight Control Panel"), + div((new Clock).element), + div("UAV " + socket.remoteSystemId) + ) + + val left = div( + panel( + table(`class` := "table-instrument")( + thead("Communication"), + tbody( + tr( + td("Uplink RSSI"), + td("89"), + td("Socket"), + td("5ms") + ), + tr( + td("Something else"), + td("unknown"), + td("Heartbeat"), + td(i(`class` := "fa fa-heart heartbeat")) + ) + ) + ), + table(`class` := "table-instrument")( + thead("Packets"), + tbody( + tr( + td("OK"), + Rx { td(socket.stats.packets()) }, + td("CRC"), + Rx { td(socket.stats.crcErrors()) }, + td("OFLW"), + Rx { td(socket.stats.overflows()) }, + td("BID"), + Rx { td(socket.stats.wrongIds()) } + ), + tr( + td("Ratio"), + Rx { + import socket.stats._ + val sum = packets() + crcErrors() + overflows() + wrongIds() + td(1.0 * packets() / sum formatted "%.2f") + }, + td(), + td(), + td(), + td(), + td(), + td() + ) + ) + ) + ), + panel( + table(`class` := "table-instrument")( + tbody( + tr( + td(compass.element), + td(horizon.element), + td(altimeter.element), + td(altimeter.element) + ) + ) + ) + ), + panel( + div(style := "width: 50%; display: inline-block;")( + table(`class` := "table-instrument")( + tbody( + tr( + td(motor1.element), + td(), + td(motor0.element) + ), + tr( + td(), + td(powerDistribution.element), + td() + ), + tr( + td(motor2.element), + td(), + td(motor3.element) + ) + ) + ) + ), + div(style := "width: 50%; display: inline-block;")( + table(`class` := "table-instrument")( + thead("Power"), + tbody( + tr( + td("VHIGH"), + td("12.6V"), + td("VLOW"), + td("9V") + ), + tr( + td("Voltage"), + td("11.2V"), + td("Remaining"), + td("80%") + ), + tr( + td("Flight"), + td("05:00"), + td("Endurance"), + td("12:00") + ) + ) + ), + table(`class` := "table-instrument")( + thead("Navigation"), + tbody( + tr( + td("Satellites"), + td("5"), + td("Precision"), + td("10cm") + ), + tr( + td("LON"), + td(""), + td("LAT"), + td("") + ), + tr( + td("GSpeed"), + td("3 m/s"), + td(), + td() + ), + tr( + td("Travelled"), + td("5000m"), + td("Home"), + td("1200m") + ) + ) + ) + ) + ) + ) + + + + val hud = { + def overlay(name: String, z: Int, thinnerThanWide: Boolean = false) = { + val direction = if (thinnerThanWide) "row" else "column" + div( + style:= + "position: absolute; left: 0; right: 0; top: 0; bottom: 0;" + + s"display: flex; align-content: center; align-items: stretch; flex-direction: $direction;"+ + s"z-index: $z;" + )( + `object`(`type`:="image/svg+xml", "data".attr:=env.asset("images/hud/" + name + ".svg"), style := "flex: 1 1 100%;") + ) + + } + + Seq( + overlay("horizon", 0), + overlay("roll", 1) + ) + } + + + + val element = div(`class` := "d-container d-column", style:="width: 100%; height: 100%;")( + div(`class` := "d-above")( + top + ), + div(`class` := "d-above d-container d-row")( + panel(modes), + panel(infos) + ), + div(`class` := "d-container d-row")( + div(`class` := "d-container d-left")( + left + ), + div(`class` := "d-main", style:="position: relative;")( + (new Hud(horizon.value)).element + ) + ) + ).render + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala new file mode 100644 index 0000000..9919d33 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala @@ -0,0 +1,19 @@ +package mavigator.dashboard.ui.instruments + +import mavigator.util.Environment + +import org.scalajs.dom.html +import rx._ + +class Altimeter(val value: Rx[Double])(implicit env: Environment) extends SvgInstrument[Double] { + import SvgInstrument._ + + lazy val element = svgObject("altimeter") + lazy val hand = part("hand") + lazy val moveable = Seq(hand) + + // 1m === 36deg = 2Pi / 10 ~= 0.62832 + protected def update(altitude: Double) = { + rotate(hand, (altitude * 0.62832).toInt) + } +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala new file mode 100644 index 0000000..0950eab --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala @@ -0,0 +1,18 @@ +package mavigator.dashboard.ui.instruments + +import mavigator.util.Environment +import org.scalajs.dom.html +import rx._ + +class Bar(val value: Rx[Double])(implicit env: Environment) extends SvgInstrument[Double] { + import SvgInstrument._ + + lazy val element = svgObject("bar") + lazy val level = part("level") + lazy val moveable = Seq(level) + + protected def update(value: Double) = { + translate(level, 0, (97 * (1 - value / 100)).toInt) + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala new file mode 100644 index 0000000..160fd2d --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala @@ -0,0 +1,23 @@ +package mavigator.dashboard.ui.instruments + +import org.scalajs.dom +import rx._ +import scala.scalajs.js.Date +import scalatags.JsDom.all._ + +class Clock extends Instrument[Date] { + + def format(date: Date) = date.toLocaleTimeString() + + val value = Var(new Date) + + val element = span(format(value())).render + + protected def update(value: Date) = { + element.innerHTML = format(value) + } + + dom.setInterval(() => {value() = new Date}, 1000) + ready() + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala new file mode 100644 index 0000000..117c7a3 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala @@ -0,0 +1,17 @@ +package mavigator.dashboard.ui.instruments + +import org.scalajs.dom.html +import rx._ +import mavigator.util.Environment + +class Compass(val value: Rx[Double])(implicit env: Environment) extends SvgInstrument[Double] { + import SvgInstrument._ + + lazy val element = svgObject("compass") + lazy val plate = part("heading") + lazy val moveable = Seq(plate) + + protected def update(heading: Double) = { + rotate(plate, heading) + } +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala new file mode 100644 index 0000000..aadafe0 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala @@ -0,0 +1,25 @@ +package mavigator.dashboard.ui.instruments + +import org.scalajs.dom.html +import rx._ +import mavigator.util.Environment + +class Distribution(val value: Rx[(Double, Double, Double, Double)])(implicit env: Environment) extends SvgInstrument[(Double, Double, Double, Double)] { + import SvgInstrument._ + + lazy val element = svgObject("distribution") + lazy val position = part("position") + lazy val moveable = Seq(position) + + private final val Radius = 50 //px + + protected def update(value: (Double, Double, Double, Double)) = { + val sum = value._1 + value._2 + value._3 + value._4 + val i = (value._1 - value._3) / sum + val j = (value._2 - value._4) / sum + val x = math.sqrt(2) / 2 * (i - j) + val y = math.sqrt(2) / 2 * (-i - j) + translate(position, (x * Radius).toInt, (y * Radius).toInt) + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala new file mode 100644 index 0000000..14f7de3 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala @@ -0,0 +1,40 @@ +package mavigator.dashboard.ui.instruments + +import org.scalajs.dom +import org.scalajs.dom.html +import rx._ +import mavigator.util.Environment + +class Generic( + min: Double, + med: Double, + max: Double, + unit: String, + val value: Rx[Double]) + (implicit env: Environment) + extends SvgInstrument[Double] { + + import SvgInstrument._ + + lazy val element = svgObject("generic") + lazy val handElement = part("hand") + lazy val unitElement = element.contentDocument.getElementById("unit") + lazy val valueElement = element.contentDocument.getElementById("value") + lazy val minElement = element.contentDocument.getElementById("min") + lazy val medElement = element.contentDocument.getElementById("med") + lazy val maxElement = element.contentDocument.getElementById("max") + lazy val moveable = Seq(handElement) + + override protected def load(e: dom.Event) = { + unitElement.textContent = unit + minElement.textContent = min.toString + medElement.textContent = med.toString + maxElement.textContent = max.toString + super.load(e) + } + + protected def update(value: Double) = { + rotate(handElement, value / (max - min) * math.Pi * 3 / 2) + valueElement.textContent = value.toString + } +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala new file mode 100644 index 0000000..249f892 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala @@ -0,0 +1,19 @@ +package mavigator.dashboard.ui.instruments + +import org.scalajs.dom.html +import rx._ +import mavigator.util.Environment + +class Horizon(val value: Rx[(Double, Double)])(implicit env: Environment) extends SvgInstrument[(Double, Double)] { + import SvgInstrument._ + + lazy val element = svgObject("horizon") + lazy val pitch = part("pitch") + lazy val roll = part("roll") + lazy val moveable = Seq(pitch, roll) + + protected def update(pitchRoll: (Double, Double)) = { + translate(pitch, 0, (pitchRoll._1 * 180 / math.Pi).toInt) // 1deg === 1px + rotate(roll, pitchRoll._2) + } +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala new file mode 100644 index 0000000..61f240c --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala @@ -0,0 +1,25 @@ +package mavigator.dashboard.ui.instruments + +import rx._ +import org.scalajs.dom.html + +/** Common trait to all flight instruments. */ +trait Instrument[A] { + + /** Current value that is displayed in the instrument. */ + val value: Rx[A] + + /** HTML element that contains the rendered instrument */ + val element: html.Element + + /** Performs the actual UI update of this instrument. */ + protected def update(newValue: A): Unit + + /** Call when instrument has finished setting up its UI. */ + protected def ready() = { + Obs(value, skipInitial = true) { + update(value()) + } + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala new file mode 100644 index 0000000..c6f7d45 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala @@ -0,0 +1,17 @@ +package mavigator.dashboard.ui.instruments + +import rx._ +import scalatags.JsDom.all._ +import mavigator.util.Environment + +class Led(val value: Rx[String])(implicit env: Environment) extends SvgInstrument[String] { + + lazy val element = `object`(`type` := "image/svg+xml", "data".attr := env.asset("images/leds/led.svg"), width := 100.pct)( + "Error loading led.").render + protected def moveable = Seq() + + protected def update(color: String) = { + part("light").setAttribute("fill", color) + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala new file mode 100644 index 0000000..28c8d65 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala @@ -0,0 +1,54 @@ +package mavigator.dashboard.ui.instruments + + +import org.scalajs.dom +import org.scalajs.dom.html + +import scalatags.JsDom.all._ + +import mavigator.util.Environment + +/** An instrument backed by an SVG image. */ +trait SvgInstrument[A] extends Instrument[A] { + + /** SVG object element that contains the rendered instrument */ + val element: html.Object + + /** Retrieves an element of the underlying SVG document by ID. */ + protected def part(id: String) = element.contentDocument.getElementById(id).asInstanceOf[html.Element] + + /** Movable parts of the instrument */ + protected def moveable: Seq[html.Element] + + /** Called when element has been loaded. */ + protected def load(event: dom.Event): Unit = { + for (part <- moveable) { + part.style.transition = "transform 50ms ease-out" + } + ready() + } + + element.addEventListener("load", (e: dom.Event) => load(e)) +} + +/** Contains helpers for SVG instruments. */ +object SvgInstrument { + + /** Retrieves an SVG object element by its instrument's name. */ + def svgObject(name: String)(implicit app: Environment): html.Object = { + val path = app.asset("images/instruments/" + name + ".svg") + `object`(`type` := "image/svg+xml", "data".attr := path, width := 100.pct)( + "Error loading instrument " + name).render + } + + /** Applies translation styling to an element. */ + def translate(elem: html.Element, x: Int, y: Int): Unit = { + elem.style.transform = "translate(" + x + "px, " + y + "px)"; + } + + /** Applies rotation styling to an element. */ + def rotate(elem: html.Element, rad: Double): Unit = { + elem.style.transform = "rotateZ(" + rad + "rad)"; + } + +} |