aboutsummaryrefslogtreecommitdiff
path: root/mavigator-cockpit/src/main/scala/mavigator/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'mavigator-cockpit/src/main/scala/mavigator/dashboard')
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala23
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala70
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala70
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala47
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala266
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala19
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala18
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala23
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala17
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala25
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala40
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala19
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala25
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala17
-rw-r--r--mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala54
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&amp;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)";
+ }
+
+}