diff options
Diffstat (limited to 'vfd-frontend/src/main/scala/vfd')
5 files changed, 351 insertions, 0 deletions
diff --git a/vfd-frontend/src/main/scala/vfd/frontend/Frontend.scala b/vfd-frontend/src/main/scala/vfd/frontend/Frontend.scala new file mode 100644 index 0000000..ffae846 --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/Frontend.scala @@ -0,0 +1,99 @@ +package vfd.frontend + + +import scala.scalajs.js +import js.annotation.JSExport + +import org.scalajs.dom +import org.scalajs.spickling._ +import org.scalajs.spickling.jsany._ + +import scalatags.JsDom.all._ + +import rx._ + +import vfd.frontend.ui._ +import vfd.frontend.util.Application +import vfd.uav.DataFrame + +@JSExport +class Frontend(rootId: String, assetsBase: String, socketUrl: String) { + + lazy val root = dom.document.getElementById(rootId) + implicit lazy val app = new Application(root, assetsBase) + + PicklerRegistry.register[vfd.uav.DataFrame] + + @JSExport + def main() = { + val connection = new dom.WebSocket(socketUrl); + val input: Var[DataFrame] = Var(DataFrame(0,0,0,0,0)) + + connection.onmessage = (e: dom.MessageEvent) => { + val json = js.JSON.parse(e.data.asInstanceOf[String]).asInstanceOf[js.Any] + val frame = PicklerRegistry.unpickle(json).asInstanceOf[vfd.uav.DataFrame] + input() = frame + } + + val element = div(`class` := "container-fluid")( + div(`class` := "row")( + div(`class` := "col-xs-12")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")( + Panels.autopilot + ) + ) + ) + ), + div(`class` := "row")( + div(`class` := "col-xs-4")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")( + Panels.secondary(input) + ) + ) + ), + div(`class` := "col-xs-5")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")( + Panels.primary(input) + ) + ) + ), + div(`class` := "col-xs-3")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")( + Panels.eicas() + ) + ) + ) + ) + ) + + root.appendChild(element.render) + + } + + + + + +/* + def alert() = { + val image = "/assets/images/leds/led.svg" + val off = "#782121" + val on = "#ff0000" + val controls = div( + `object`("data".attr := image, `type` := "image/svg+xml", width:= "32px")( + "Cannot load" + ), + audio( + "autoplay".attr:="true", + source(src:="/assets/audio/alarm.ogg", `type`:="audio/ogg") + ) + + ).render + controls + }*/ + +}
\ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala new file mode 100644 index 0000000..959ca8a --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala @@ -0,0 +1,67 @@ +package vfd.frontend.ui + +import rx._ +import scalatags.JsDom.all._ +import vfd.frontend.util.Application +import org.scalajs.dom.HTMLElement + +object Components { + + def led(color: Rx[String], size: String)(implicit app: Application) = { + val elem = `object`(`type` := "image/svg+xml", "data".attr := app.asset("leds/led.svg"), width := size)( + "Error loading image." + ).render + + Obs(color, skipInitial = true) { + val svg = elem.contentDocument + svg.getElementById("light").setAttribute("fill", color()) + } + elem + } + + private def instrument(name: String)(implicit app: Application) = { + val path = app.asset("images/instruments/" + name + ".svg") + `object`(`type` := "image/svg+xml", "data".attr := path, width := "100%")( + "Error loading image " + name + ).render + } + + private def frame(elem: HTMLElement, size: String) = { + div(style := s"width: $size; height: $size; display: inline-block;" )( + elem + ) + } + + def attitude(pitchRoll: Rx[(Double, Double)], size: String)(implicit app: Application) = { + val inst = instrument("attitude") + Obs(pitchRoll, skipInitial = true){ + val svg = inst.contentDocument + val pitch = svg.getElementById("pitch") + val roll = svg.getElementById("roll") + pitch.setAttribute("transform", "translate(0, " + pitchRoll()._1 / math.Pi * 180 + ")"); + roll.setAttribute("transform", "rotate(" + pitchRoll()._2 / math.Pi * 180 + ")"); + } + frame(inst, size) + } + + def altitude(value: Rx[Double], size: String)(implicit app: Application) = { + val inst = instrument("altitude") + Obs(value, skipInitial = true){ + val svg = inst.contentDocument + // 36deg === 1m + svg.getElementById("hand").setAttribute("transform", "rotate(" + value() * 36 + ")"); + } + frame(inst, size) + } + + def heading(value: Rx[Double], size: String)(implicit app: Application) = { + val inst = instrument("heading") + Obs(value, skipInitial = true){ + val svg = inst.contentDocument + // 1deg === 1deg + svg.getElementById("heading").setAttribute("transform", "rotate(" + value() / math.Pi * 180 + ")"); + } + frame(inst, size) + } + +}
\ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/ui/Panels.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/Panels.scala new file mode 100644 index 0000000..8c5e38d --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/Panels.scala @@ -0,0 +1,116 @@ +package vfd.frontend.ui + +import rx._ +import rx.ops._ +import scalatags.JsDom.all._ +import vfd.uav.DataFrame +import vfd.frontend.util.Application +import vfd.frontend.util.Framework._ + + +object Panels { + + def primary(input: Rx[DataFrame])(implicit app: Application) = div( + Components.heading(input map (_.heading), "33%"), + Components.attitude(input map (i => (i.pitch, i.roll)), "33%"), + Components.altitude(input map (_.altitude), "33%") + ) + + def secondary(input: Rx[DataFrame])(implicit app: Application) = div( + iframe( + width:="100%", + height:="350px", + "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" + ), + table(`class`:="table")( + tr( + td("UAV Position"), + td("N13.1234 E1234.23465") + ), + tr( + td("Base Position"), + td("N13.1234 E1234.23465") + ), + tr( + td("Distance to UAV"), + td("200 m") + ), + tr( + td("Total flight distance"), + td("12.3 km") + ), + tr( + td("Groundspeed"), + td("23 km/h") + ), + tr( + td("---"), + td("") + ), + tr( + td("Below"), + td("180 cm") + ) + ) + ) + + def eicas()(implicit app: Application) = { + table(`class`:="table")( + tr( + td("Link Server"), + td("3"), + td("ms"), + td(img(src:="/assets/images/leds/red-off.svg",width:="16px")) + ), + tr( + td("Link UAV"), + td("-80"), + td("dB(m)"), + td(img(src:="/assets/images/leds/red-on.svg",width:="16px")) + ), + tr( + td("---"), + td(""), + td(""), + td("") + ), + tr( + td("Charge"), + td("4.800"), + td("Ah"), + td(img(src:="/assets/images/leds/red-off.svg",width:="16px")) + ), + tr( + td("Current"), + td("80"), + td("A"), + td(img(src:="/assets/images/leds/yellow-on.svg",width:="16px")) + ), + tr( + td("Endurance"), + td("14"), + td("min"), + td(img(src:="/assets/images/leds/none.svg",width:="16px")) + ), + tr( + td("GPS"), + td("5"), + td("satellites"), + td(img(src:="/assets/images/leds/none.svg",width:="16px")) + ) + ) + } + + def autopilot = div(`class` := "btn-group")( + button(`type`:="button", `class`:="btn btn-default")("Auto"), + button(`type`:="button", `class`:="btn btn-default")("Position"), + button(`type`:="button", `class`:="btn btn-default")("Attitude"), + button(`type`:="button", `class`:="btn btn-default")( + span(`class`:="label label-default")("Manual") + ) + ) +}
\ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/util/Application.scala b/vfd-frontend/src/main/scala/vfd/frontend/util/Application.scala new file mode 100644 index 0000000..3b4e733 --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/util/Application.scala @@ -0,0 +1,11 @@ +package vfd.frontend.util + +import org.scalajs.dom.HTMLElement + +class Application(element: HTMLElement, assetsBase: String) { + + def root = element + + def asset(file: String): String = assetsBase + "/" + file + +}
\ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/util/Framework.scala b/vfd-frontend/src/main/scala/vfd/frontend/util/Framework.scala new file mode 100644 index 0000000..76dceea --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/util/Framework.scala @@ -0,0 +1,58 @@ +package vfd.frontend.util + +import scala.collection.{SortedMap, mutable} +import scalatags.JsDom.all._ +import scala.util.{Failure, Success, Random} +import rx._ +import rx.core.{Propagator, Obs} +import org.scalajs.dom +import org.scalajs.dom.{Element, DOMParser} +import scala.scalajs.js +import scala.language.implicitConversions + +/** + * A minimal binding between Scala.Rx and Scalatags and Scala-Js-Dom + * taken from https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/Framework.scala, by Li Haoyi + */ +object Framework { + + /** + * Wraps reactive strings in spans, so they can be referenced/replaced + * when the Rx changes. + */ + implicit def RxStr[T](r: Rx[T])(implicit f: T => Frag): Frag = { + rxMod(Rx(span(r()))) + } + + /** + * 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 <: dom.HTMLElement](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) + } + + implicit def RxAttrValue[T: AttrValue] = new AttrValue[Rx[T]]{ + def apply(t: Element, a: Attr, r: Rx[T]): Unit = { + Obs(r){ implicitly[AttrValue[T]].apply(t, a, r())} + } + } + + implicit def RxStyleValue[T: StyleValue] = new StyleValue[Rx[T]]{ + def apply(t: Element, s: Style, r: Rx[T]): Unit = { + Obs(r){ implicitly[StyleValue[T]].apply(t, s, r())} + } + } +} |