From dae336ee5514970fee93b0db25ce272ab83bd3c7 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Sat, 27 Feb 2016 17:11:10 -0800 Subject: Refactor UI to a HUD-style interface --- CONTRIBUTING.md | 7 - .../src/main/scala/mavigator/cockpit/Cockpit.scala | 26 + .../main/scala/mavigator/cockpit/Instruments.scala | 173 +++++ .../src/main/scala/mavigator/cockpit/Layout.scala | 54 ++ .../mavigator/cockpit/MavlinkWebSockets.scala | 60 ++ .../src/main/scala/mavigator/dashboard/Main.scala | 23 - .../scala/mavigator/dashboard/MavlinkSocket.scala | 70 -- .../main/scala/mavigator/dashboard/RxUtil.scala | 70 -- .../main/scala/mavigator/dashboard/ui/Hud.scala | 47 -- .../main/scala/mavigator/dashboard/ui/Layout.scala | 266 ------- .../dashboard/ui/instruments/Altimeter.scala | 19 - .../mavigator/dashboard/ui/instruments/Bar.scala | 18 - .../mavigator/dashboard/ui/instruments/Clock.scala | 23 - .../dashboard/ui/instruments/Compass.scala | 17 - .../dashboard/ui/instruments/Distribution.scala | 25 - .../dashboard/ui/instruments/Generic.scala | 40 - .../dashboard/ui/instruments/Horizon.scala | 19 - .../dashboard/ui/instruments/Instrument.scala | 25 - .../mavigator/dashboard/ui/instruments/Led.scala | 17 - .../dashboard/ui/instruments/SvgInstrument.scala | 54 -- .../main/scala/mavigator/index/ActiveVehicle.scala | 3 +- .../src/main/scala/mavigator/index/Main.scala | 71 +- .../src/main/scala/mavigator/index/Util.scala | 6 +- .../main/scala/mavigator/util/Application.scala | 12 +- .../main/scala/mavigator/util/Environment.scala | 15 + .../src/main/scala/mavigator/util/Page.scala | 38 + .../main/scala/mavigator/util/environment.scala | 24 - .../main/resources/assets/images/hud/attitude.svg | 815 +++++++++++++++++++++ .../src/main/resources/assets/images/hud/hud.svg | 182 ----- .../main/resources/assets/images/hud/overlay.html | 29 - .../src/main/resources/assets/images/hud/roll.svg | 143 ---- .../src/main/resources/assets/stylesheets/main.css | 273 +++---- .../main/resources/assets/stylesheets/reset.css | 48 ++ .../src/main/scala/mavigator/Router.scala | 4 +- .../src/main/twirl/mavigator/views/app.scala.html | 12 +- .../main/twirl/mavigator/views/index.scala.html | 5 - .../src/main/twirl/mavigator/views/main.scala.html | 5 +- .../src/main/scala/mavigator/uav/MavlinkUtil.scala | 46 -- .../scala/mavigator/uav/MavlinkUtil.scala.disabled | 46 ++ .../src/main/scala/mavigator/uav/Multiplexer.scala | 20 - .../scala/mavigator/uav/Multiplexer.scala.disabled | 20 + .../src/main/scala/mavigator/uav/Uav.scala | 2 +- .../mavigator/uav/mock/RandomFlightPlan.scala | 3 +- project/Dependencies.scala | 4 +- project/Js.scala | 2 +- project/plugins.sbt | 7 +- 46 files changed, 1480 insertions(+), 1408 deletions(-) delete mode 100644 CONTRIBUTING.md create mode 100644 mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala create mode 100644 mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala create mode 100644 mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala create mode 100644 mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala create mode 100644 mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala create mode 100644 mavigator-cockpit/src/main/scala/mavigator/util/Page.scala delete mode 100644 mavigator-cockpit/src/main/scala/mavigator/util/environment.scala create mode 100644 mavigator-server/src/main/resources/assets/images/hud/attitude.svg delete mode 100644 mavigator-server/src/main/resources/assets/images/hud/hud.svg delete mode 100644 mavigator-server/src/main/resources/assets/images/hud/overlay.html delete mode 100644 mavigator-server/src/main/resources/assets/images/hud/roll.svg create mode 100644 mavigator-server/src/main/resources/assets/stylesheets/reset.css delete mode 100644 mavigator-server/src/main/twirl/mavigator/views/index.scala.html delete mode 100644 mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala create mode 100644 mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala.disabled delete mode 100644 mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala create mode 100644 mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala.disabled diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index a95c3bb..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -Thanks for taking the time to help out! - -# Pull Requests -Please follow these basic guidelines when submitting pull requests: - - Follow the Scala style guide - - Keep commit history clean by squashing non-relevant commits and rebasing onto master prior to a pull request - - Use the present tense in commit messages ("Add feature" not "Added feature") \ No newline at end of file diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala new file mode 100644 index 0000000..f8aba76 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala @@ -0,0 +1,26 @@ +package mavigator +package cockpit + +import scala.scalajs.js.annotation.JSExport + +import mavigator.util.{Environment, Application} +import scalatags.JsDom.all._ +import util._ + +class Cockpit + extends Page + with Layout + with MavlinkWebSockets + with Instruments + +@JSExport("mavigator_cockpit_Main") +object Cockpit extends Application { + + override def main(args: Map[String, String])(implicit env: Environment): Unit = { + val app = new Cockpit + app.attach(env) + app.connect(args("socketUrl"), args("remoteSystemId").toInt) + } + +} + diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala new file mode 100644 index 0000000..7c4bf92 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala @@ -0,0 +1,173 @@ +package mavigator +package cockpit + +import org.scalajs.dom +import org.scalajs.dom.html + +import scalatags.JsDom.all._ + +import util.Page + +trait Instruments { page: Page => + + trait Instrument[A] { + def element: html.Element + def update(newValue: A): Unit + } + + /** Common behaviour for svg-based instruments. */ + abstract class SvgInstrument[A]( + path: String + ) extends Instrument[A] { + + override def element: html.Element = objectElement + + /** SVG object element that contains the rendered instrument */ + lazy val objectElement: html.Object = SvgInstrument.svgObject(path) + + /** Retrieves an element of the underlying SVG document by ID. */ + protected def part(id: String): html.Element = + objectElement.contentDocument.getElementById(id).asInstanceOf[html.Element] + + /** Movable parts of the instrument */ + protected def moveable: Seq[html.Element] + + /** Called when element has been loaded. */ + private def load(event: dom.Event): Unit = { + for (part <- moveable) { + part.style.transition = "transform 20ms ease-out" + } + } + + 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(path: String): html.Object = { + val fullPath = page.asset("images/" + path) + `object`(`type` := "image/svg+xml", "data".attr := fullPath, width := 100.pct)( + "Error loading instrument " + fullPath).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)"; + } + + } + + lazy val attitudeOverlay = new SvgInstrument[(Float, Float)]("hud/attitude.svg") { + override def element = div(`class`:="hud-overlay")(objectElement).render + private lazy val pitchPart = part("pitch") + private lazy val rollPart = part("roll") + override lazy val moveable = Seq(pitchPart, rollPart) + + override def update(pitchRoll: (Float, Float)) = { + import SvgInstrument._ + val (pitch, roll) = pitchRoll + translate(pitchPart, 0, (pitch * 180 / math.Pi * 10).toInt) // 1deg === 10px + rotate(rollPart, roll) + } + } + + lazy val horizonOverlay = new SvgInstrument[(Float, Float)]("hud/horizon.svg") { + import SvgInstrument._ + + override def element = div(`class`:="hud-overlay")(objectElement).render + lazy val horizon = part("horizon") + lazy override val moveable = Seq(horizon) + + override def update(pitchRoll: (Float, Float)) = { + val (pitch, roll) = pitchRoll + + val t = (pitch * 180 / math.Pi * 10).toInt // 1deg === 10px + + horizon.style.transform = s"rotateZ(${roll}rad)translate(0px, ${t}px) " + } + } + + + val overlayStyle = """ + |.hud-overlay { + | position: absolute; + | left: 0; + | right: 0; + | top: 0; + | bottom: 0; + | + | display: flex; + | flex-direction: row; + | justify-content: center; + | align-items: center; + |} + |.hud-overlay > * { + | flex: 1 1 0; + | width: 100%; + | height: 100%; + | max-width: 100%; + | max-height: 100%; + |}""".stripMargin + + + def mode(name: String, kind: String, on: Boolean = false) = { + div(`class` := s"mode $kind ${if (!on) "off"}")(name) + } + + //TODO make these into real instruments and lazy vals + def modes = div(style := "float: right;")( + mode("LINK", "danger", true), + mode("BAT", "warning", true), + mode("GPS", "warning", true), + mode("STABILIZED", "info", true) + ) + + val modeStyle = """ +.mode { + display: inline-block; + box-sizing: border-box; + text-decoration: normal; + margin-right: 5px; + padding: 5px; +} + +.mode.danger { + color: #d9534f; + text-shadow: 0 0 5px #d9534f; + animation: danger-blink 0.5s linear infinite; + -webkit-animation: danger-blink 0.5s linear infinite; +} + +.mode.warning { + color: #f0ad4e; + text-shadow: 0 0 5px #f0ad4e; +} + +.mode.info { + color: #5bc0de; + text-shadow: 0 0 5px #5bc0de; +} + +.mode.success { + color: #5cb85c; + text-shadow: 0 0 5px #5cb85c; +} + +.mode.off { + color: #e6e6e6; + text-shadow: none; + animation: none; + -webkit-animation: none; +} +""" + + def instrumentStyles: Seq[String] = Seq(overlayStyle, modeStyle) + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala new file mode 100644 index 0000000..887c4d6 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala @@ -0,0 +1,54 @@ +package mavigator +package cockpit + +import org.scalajs.dom.html +import scalatags.JsDom.all._ + +import util._ + +/** Provides main cockpit layout. */ +trait Layout { self: Page with Instruments => + + /** Elements to display in the mode control pannel (top panel). */ + def mcp = div(id := "mcp")( + img(src := asset("images/logo-invert.svg"), style:="height: 20px; margin: 5px;"), + span(`class`:="mode warning")("Demo System"), + modes + ) + + /** Element to deisplay on heads-up display (main area). */ + def hud = div(id :="hud")( + attitudeOverlay.element + ) + + val layoutStyle = """ + |#cockpit { + | width: 100%; + | height: 100%; + | display: flex; + | flex-direction: column; + | justify-content: flex-start; + | align-items: stretch; + | + | background-color: pink; + |} + | + |#mcp { + | flex: 0 1 0; + | background-color: #222222; + |} + | + |#hud { + | flex: 1 1 auto; + | position: relative; + | background-color: lightblue; + |}""".stripMargin + + override def styles = Seq(layoutStyle) ++ instrumentStyles + + override def elements: Seq[html.Element] = Seq(div(id := "cockpit")( + mcp, + hud + ).render) + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala new file mode 100644 index 0000000..3797005 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala @@ -0,0 +1,60 @@ +package mavigator +package cockpit + +import org.mavlink._ +import org.mavlink.Parser.Errors._ +import org.mavlink.messages._ + +import org.scalajs.dom +import scalajs.js + +trait MavlinkWebSockets { self: Instruments => + + private class MavlinkWebSocket(url: String, remoteSystemId: Int) { + + private var _open = false + def open: Boolean = _open + + private val parser = new Parser( + { + case pckt@Packet(seq, `remoteSystemId`, compId, msgId, payload) => + val msg = Message.unpack(pckt.messageId, pckt.payload) + onMessage(msg) + //_packets() = _packets.now + 1 + case _ => + //_wrongIds() = _wrongIds.now + 1 + }, + { + case CrcError => //_crcErrors() = _crcErrors.now + 1 + case OverflowError => //_overflows() = _overflows.now + 1 + } + ) + + private val connection = new dom.WebSocket(url) + + connection.binaryType = "arraybuffer" + + connection.onopen = (e: dom.Event) => { + _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) => { + _open = false + } + + } + + private def onMessage(msg: Message): Unit = msg match { + case a: Attitude => attitudeOverlay.update((a.pitch.toFloat, a.roll.toFloat)) + case _ => () + } + + def connect(url: String, remoteSystemId: Int): Unit = new MavlinkWebSocket(url, remoteSystemId) +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala deleted file mode 100644 index f3a111a..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 7f4ffdc..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 2637ddf..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 18c5c27..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 902cb9c..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -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 deleted file mode 100644 index 9919d33..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 0950eab..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 160fd2d..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 117c7a3..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index aadafe0..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 14f7de3..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 249f892..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 61f240c..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index c6f7d45..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 28c8d65..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala +++ /dev/null @@ -1,54 +0,0 @@ -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)"; - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala b/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala index 870d3be..7e59ed8 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala @@ -13,7 +13,8 @@ object ActiveVehicle { id, vehicleType(hb.`type`), autopilot(hb.autopilot), - state(hb.systemStatus)) + state(hb.systemStatus) + ) def vehicleType(tpe: Int) = tpe match { case MavType.MavTypeGeneric => "Generic" diff --git a/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala b/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala index 7724f89..452d517 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala @@ -5,7 +5,6 @@ import scala.scalajs.js.annotation.JSExport import org.scalajs.dom.html -import mavigator.dashboard.ui.Layout import mavigator.util.Environment import mavigator.util.Application @@ -29,16 +28,17 @@ object Main extends Application { val parser = new Parser( packet => { val m: Message = Message.unpack(packet.messageId, packet.payload) - println(m) m match { case hb: Heartbeat => - active() += ActiveVehicle.fromHeartbeat(packet.systemId, hb) + active() = active.now + ActiveVehicle.fromHeartbeat(packet.systemId, hb) case _ => () } } ) override def main(args: Map[String, String])(implicit env: Environment): Unit = { + import rx.Ctx.Owner.Unsafe._ + val root = env.root val connection = new dom.WebSocket(args("socketUrl")) @@ -64,34 +64,45 @@ object Main extends Application { } - root.appendChild(div( - table(`class` := "table table-hover")( - thead( - tr( - th("System ID"), - th("Type"), - th("Autopilot"), - th("State"), - th("") - ) - ), - Rx { - tbody( - for (vehicle <- active().toSeq) yield { - tr( - td(vehicle.systemId), - td(vehicle.vehicleType), - td(vehicle.autopilot), - td(vehicle.state), - td(a(href := "/dashboard/" + vehicle.systemId, `class` := "btn btn-default")("Pilot")) - ) - } + val elem = + div(`class` := "container")( + div(`class` := "col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3")( + div(`class` := "panel panel-default")( + div(`class` := "panel-heading")( + h3(`class` := "panel-title")("Available vehicles") + ), + div(`class` := "panel-body")( + table(`class` := "table table-hover")( + thead( + tr( + th("System ID"), + th("Type"), + th("Autopilot"), + th("State"), + th("") + ) + ), + Rx { + tbody( + for (vehicle <- active().toSeq) yield { + tr( + td(vehicle.systemId), + td(vehicle.vehicleType), + td(vehicle.autopilot), + td(vehicle.state), + td(a(href := "/cockpit/" + vehicle.systemId, `class` := "btn btn-default")("Pilot")) + ) + } + ) + } + ), + p(i(`class`:="fa fa-spinner fa-spin")(), " Listening for heartbeats...") + ) ) - } - ), - i(`class`:="fa fa-spinner fa-spin")(), - " Listening for heartbeats..." - ).render) + ) + ).render + + root.appendChild(elem) } } diff --git a/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala b/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala index 16f3d5d..2671f2f 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala @@ -13,7 +13,7 @@ import scala.util.Failure import scalatags.JsDom.all._ object Util { - + /** * Copied from https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/Framework.scala * @@ -22,13 +22,13 @@ object Util { * 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 = { + implicit def rxMod(r: Rx[HtmlTag])(implicit owner: Ctx.Owner): 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) { + r.triggerLater{ val newLast = rSafe last.parentElement.replaceChild(newLast, last) last = newLast diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala b/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala index 7cb6f27..68a39b1 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala @@ -4,6 +4,7 @@ package util import scala.scalajs.js.annotation.JSExport import scala.scalajs.js +import org.scalajs.dom.console import org.scalajs.dom.html trait Application { @@ -13,13 +14,18 @@ trait Application { @JSExport final def start(settings: js.Dynamic): Unit = { - val env = new StaticEnvironment( + console.info("Initializing environment,,,") + val env = new Environment( root = settings.root.asInstanceOf[html.Element], - assetsBase = settings.assetsBase.asInstanceOf[String] + styleRoot = settings.styleRoot.asInstanceOf[html.Element], + baseUrl = settings.baseUrl.asInstanceOf[String] ) - val args = settings.args.asInstanceOf[js.Dictionary[Any]].mapValues(_.toString).toMap + console.info("Reading arguments...") + val args: Map[String, String] = + settings.args.asInstanceOf[js.Dictionary[Any]].mapValues(_.toString).toMap + console.info("Entering main...") main(args)(env) } diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala b/mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala new file mode 100644 index 0000000..b1076fc --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala @@ -0,0 +1,15 @@ +package mavigator +package util + +import org.scalajs.dom +import org.scalajs.dom.html + +/** Represents an application's environment + * @param root The application's root element. + * @param styleRoot An html 'style' tag to which app-specific styles are appended. + * @param baseUrl Base URL. */ +case class Environment( + root: html.Element, + styleRoot: html.Element, + baseUrl: String +) diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/Page.scala b/mavigator-cockpit/src/main/scala/mavigator/util/Page.scala new file mode 100644 index 0000000..f8f0dee --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/util/Page.scala @@ -0,0 +1,38 @@ +package mavigator +package util + +import org.scalajs.dom +import org.scalajs.dom.html + +/** Contents of a single-page app, consisting of styles and a main element. */ +trait Page { + + private var isAttached = false + private def attached[A](action: => A) = if (!isAttached) { + sys.error("Page has not been attached to an environment yet.") + } else { + action + } + + private var baseUrl: String = null + + def asset(path: String): String = attached { baseUrl + "/" + path } + + def styles: Seq[String] + + def elements: Seq[html.Element] + + /** Attach this page to a website. */ + def attach(env: Environment): Unit = { + baseUrl = env.baseUrl + isAttached = true + + val styleText = dom.document.createTextNode(styles.reduce(_ + _)) + env.styleRoot.appendChild(styleText) + + for (elem <- elements) { + env.root.appendChild(elem) + } + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/environment.scala b/mavigator-cockpit/src/main/scala/mavigator/util/environment.scala deleted file mode 100644 index 3d58c5a..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/util/environment.scala +++ /dev/null @@ -1,24 +0,0 @@ -package mavigator -package util - -import org.scalajs.dom.html - -/** Represents an application's environment */ -trait Environment { - - /** The application's root element. */ - def root: html.Element - - /** Retrieve an asset's URL based on its file location. */ - def asset(file: String): String - -} - -class StaticEnvironment( - override val root: html.Element, - assetsBase: String -) extends Environment { - - override def asset(file: String): String = assetsBase + "/" + file - -} diff --git a/mavigator-server/src/main/resources/assets/images/hud/attitude.svg b/mavigator-server/src/main/resources/assets/images/hud/attitude.svg new file mode 100644 index 0000000..eb7dd70 --- /dev/null +++ b/mavigator-server/src/main/resources/assets/images/hud/attitude.svg @@ -0,0 +1,815 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + 10 + 20 + 30 + 10 + 20 + 30 + 30 + 20 + 10 + 10 + 20 + 30 + + 0 + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 60 + 30 + 0 + 30 + 60 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mavigator-server/src/main/resources/assets/images/hud/hud.svg b/mavigator-server/src/main/resources/assets/images/hud/hud.svg deleted file mode 100644 index 943d7a4..0000000 --- a/mavigator-server/src/main/resources/assets/images/hud/hud.svg +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mavigator-server/src/main/resources/assets/images/hud/overlay.html b/mavigator-server/src/main/resources/assets/images/hud/overlay.html deleted file mode 100644 index 58b0ef7..0000000 --- a/mavigator-server/src/main/resources/assets/images/hud/overlay.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - -
-
side menu
- -
-
- level0 -
- -
- level0 -
-
-
- - diff --git a/mavigator-server/src/main/resources/assets/images/hud/roll.svg b/mavigator-server/src/main/resources/assets/images/hud/roll.svg deleted file mode 100644 index 93a9f90..0000000 --- a/mavigator-server/src/main/resources/assets/images/hud/roll.svg +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - diff --git a/mavigator-server/src/main/resources/assets/stylesheets/main.css b/mavigator-server/src/main/resources/assets/stylesheets/main.css index f254449..6529940 100644 --- a/mavigator-server/src/main/resources/assets/stylesheets/main.css +++ b/mavigator-server/src/main/resources/assets/stylesheets/main.css @@ -1,203 +1,146 @@ -html, body { +html, body, .app { width: 100%; height: 100%; } body { - background-color: #e6e6e6; + background-color: #e6e6e6; } .loader { - width: 100%; - font-size: 50px; - text-align: center; -} - -#vfd-dashboard { - width: 100%; - height: 100%; -} - -#vfd-dashboard header { - color: #eeeeee; - background-color: #222222; - padding-left: 8px; - padding-right: 8px; - padding-top: 3px; - padding-bottom: 3px; - margin-bottom: 3px; - display: flex; -} - -#vfd-dashboard header > * { - margin: 5px; - flex: 1; -} - -#vfd-dashboard header > :nth-child(1) { - text-align: left; -} - -#vfd-dashboard header > :nth-child(2) { - text-align: center; -} - -#vfd-dashboard header > :nth-child(3) { - text-align: right; -} - -/* dashboard layout */ -.d-container { - display: flex; - align-content: flex-start; - align-items: stretch; -} - -.d-container > * { - flex: 1; -} - -.d-column { - flex-direction: column; -} - -.d-row { - flex-direction: row; -} - -.d-above { - flex: none; -} - -.d-left { - flex: 1 1 30%; -} - -.d-main { - flex: 1 1 70%; -} - -.d-panel { - margin: 3px; - padding: 10px; - background-color: white; - border-radius: 3px; -} - + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; -/* Mode styles */ -.mode { - display: inline-block; - box-sizing: border-box; - text-decoration: normal; - margin-right: 5px; + font-size: 50px; + text-align: center; } -.mode.danger { +@keyframes danger-blink { + 0% { + color: #222222; + text-shadow: none; + } + 50% { color: #d9534f; text-shadow: 0 0 5px #d9534f; - animation: danger-blink 1s linear infinite; - -webkit-animation: danger-blink 1s linear infinite; -} - -.mode.warning { - color: #f0ad4e; - text-shadow: 0 0 5px #f0ad4e; + } + 100% { + color: #222222; + text-shadow: none; + } } -.mode.info { - color: #5bc0de; - text-shadow: 0 0 5px #5bc0de; +@-webkit-keyframes danger-blink { + 0% { + color: #eeeeee; + text-shadow: none; + } + 50% { + color: #d9534f; + text-shadow: 0 0 5px #d9534f; + } + 100% { + color: #eeeeee; + text-shadow: none; + } } -.mode.success { - color: #5cb85c; - text-shadow: 0 0 5px #5cb85c; +#map { + position: absolute; + top: auto; + left: auto; + right: 10px; + bottom: 10px; } -.mode.off { - color: #eeeeee; - text-shadow: none; - animation: none; - -webkit-animation: none; +#motors { + position: absolute; + top: auto; + bottom: 10px; + left: 10px; + right: auto; } -/* TODO: Rules below are maybe obsolete and need to be reviewed */ .table-instrument { - table-layout: fixed; - width: 100%; + table-layout: fixed; + width: 100%; } .table-instrument td { - width: 100%; + width: 100%; } .heartbeat { - color: rgba(165, 25, 25, 1); - animation: heartbeat 2s linear infinite; - -webkit-animation: heartbeat 2s linear infinite; + color: rgba(165, 25, 25, 1); + animation: heartbeat 2s linear infinite; + -webkit-animation: heartbeat 2s linear infinite; } @keyframes heartbeat { - 0% { - transform: scale(1); - } - 7% { - transform: scale(1.3); - } - 14% { - transform: scale(1); - } - 21% { - transform: scale(1.3); - } - 28% { - transform: scale(1); - } + 0% { + transform: scale(1); + } + 7% { + transform: scale(1.3); + } + 14% { + transform: scale(1); + } + 21% { + transform: scale(1.3); + } + 28% { + transform: scale(1); + } } @-webkit-keyframes heartbeat { - 0% { - transform: scale(1); - } - 7% { - transform: scale(1.3); - } - 14% { - transform: scale(1); - } - 21% { - transform: scale(1.3); - } - 28% { - transform: scale(1); - } + 0% { + transform: scale(1); + } + 7% { + transform: scale(1.3); + } + 14% { + transform: scale(1); + } + 21% { + transform: scale(1.3); + } + 28% { + transform: scale(1); + } } @keyframes danger-blink { - 0% { - color: #eeeeee; - text-shadow: none; - } - 50% { - color: #d9534f; - text-shadow: 0 0 5px #d9534f; - } - 100% { - color: #eeeeee; - text-shadow: none; - } + 0% { + color: #eeeeee; + text-shadow: none; + } + 50% { + color: #d9534f; + text-shadow: 0 0 5px #d9534f; + } + 100% { + color: #eeeeee; + text-shadow: none; + } } @-webkit-keyframes danger-blink { - 0% { - color: #eeeeee; - text-shadow: none; - } - 50% { - color: #d9534f; - text-shadow: 0 0 5px #d9534f; - } - 100% { - color: #eeeeee; - text-shadow: none; - } -} \ No newline at end of file + 0% { + color: #eeeeee; + text-shadow: none; + } + 50% { + color: #d9534f; + text-shadow: 0 0 5px #d9534f; + } + 100% { + color: #eeeeee; + text-shadow: none; + } +} +*/ diff --git a/mavigator-server/src/main/resources/assets/stylesheets/reset.css b/mavigator-server/src/main/resources/assets/stylesheets/reset.css new file mode 100644 index 0000000..af94440 --- /dev/null +++ b/mavigator-server/src/main/resources/assets/stylesheets/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/mavigator-server/src/main/scala/mavigator/Router.scala b/mavigator-server/src/main/scala/mavigator/Router.scala index 07e40b0..389cb4d 100644 --- a/mavigator-server/src/main/scala/mavigator/Router.scala +++ b/mavigator-server/src/main/scala/mavigator/Router.scala @@ -22,11 +22,11 @@ object Router { val socketUrl = "ws://localhost:8080/mavlink" def route(implicit system: ActorSystem): Route = ( - path("dashboard" / IntNumber) { id => + path("cockpit" / IntNumber) { id => get { val html = mavigator.views.html.app( "Mavigator", - "mavigator_dashboard_Main", + "mavigator_cockpit_Main", Map( "socketUrl" -> socketUrl, "remoteSystemId" -> "0", diff --git a/mavigator-server/src/main/twirl/mavigator/views/app.scala.html b/mavigator-server/src/main/twirl/mavigator/views/app.scala.html index 34d1fde..34359f8 100644 --- a/mavigator-server/src/main/twirl/mavigator/views/app.scala.html +++ b/mavigator-server/src/main/twirl/mavigator/views/app.scala.html @@ -13,9 +13,9 @@

The error was: ""

-
+
- +
@@ -30,10 +30,12 @@ root0.removeChild(root0.firstChild); } - //run ScalaJS application + //run ScalaJS application + console.info("[@appId] Starting ScalaJS application...") @{appId}().start({ root: root0, - assetsBase: "/assets", + styleRoot: document.getElementById("app-styles"), + baseUrl: "/assets", args: {@args.map{ case (key, value) => @key: "@value", }} @@ -48,6 +50,6 @@ }); - + } diff --git a/mavigator-server/src/main/twirl/mavigator/views/index.scala.html b/mavigator-server/src/main/twirl/mavigator/views/index.scala.html deleted file mode 100644 index 1bf892b..0000000 --- a/mavigator-server/src/main/twirl/mavigator/views/index.scala.html +++ /dev/null @@ -1,5 +0,0 @@ -@() - -@main("Index"){ -

Hello world!

-} diff --git a/mavigator-server/src/main/twirl/mavigator/views/main.scala.html b/mavigator-server/src/main/twirl/mavigator/views/main.scala.html index e3e83b8..1e21b1b 100644 --- a/mavigator-server/src/main/twirl/mavigator/views/main.scala.html +++ b/mavigator-server/src/main/twirl/mavigator/views/main.scala.html @@ -5,15 +5,18 @@ - + + + Mavigator - @title + diff --git a/mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala b/mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala deleted file mode 100644 index 0fb56b0..0000000 --- a/mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala +++ /dev/null @@ -1,46 +0,0 @@ -package mavigator.uav - -import org.mavlink.Assembler -import org.mavlink.Packet -import org.mavlink.Parser -import org.mavlink.messages.Message - -import akka.actor.Actor -import akka.actor.ActorLogging -import akka.util.ByteString - -/** Provides utilities for actors representing a mavlink connection. */ -trait MavlinkUtil { myself: Actor with ActorLogging => - - /** Mavlink system ID of this connection. */ - val systemId: Byte - - /** Mavlink component ID of this connection. */ - val componentId: Byte - - /** Assembler for creating packets originating from this connection. */ - private lazy val assembler = new Assembler(systemId, componentId) - - /** Assembles a message into a bytestring representing a packet sent from this connection. */ - protected def assemble(message: Message): ByteString = { - val (messageId, payload) = Message.pack(message) - val packet: Packet = assembler.assemble(messageId, payload) - ByteString(packet.toArray) - } - - /** Parser for messages being sent to the uav. */ - protected val outgoing: Parser = new Parser(packet => Message.unpack(packet.messageId, packet.payload) match { - //TODO handle ping - /* - case Ping(`systemId`, `componentId`) => - val message = Ack(packet.systemId, packet.componentId) - val data = assemble(message) - self ! Connection.Received(data)*/ - case _ => () - }) - - /** Parser for messages coming from the uav. */ - protected val incoming: Parser = new Parser(pckt => - log.debug("incoming message: " + Message.unpack(pckt.messageId, pckt.payload))) - -} diff --git a/mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala.disabled b/mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala.disabled new file mode 100644 index 0000000..0fb56b0 --- /dev/null +++ b/mavigator-uav/src/main/scala/mavigator/uav/MavlinkUtil.scala.disabled @@ -0,0 +1,46 @@ +package mavigator.uav + +import org.mavlink.Assembler +import org.mavlink.Packet +import org.mavlink.Parser +import org.mavlink.messages.Message + +import akka.actor.Actor +import akka.actor.ActorLogging +import akka.util.ByteString + +/** Provides utilities for actors representing a mavlink connection. */ +trait MavlinkUtil { myself: Actor with ActorLogging => + + /** Mavlink system ID of this connection. */ + val systemId: Byte + + /** Mavlink component ID of this connection. */ + val componentId: Byte + + /** Assembler for creating packets originating from this connection. */ + private lazy val assembler = new Assembler(systemId, componentId) + + /** Assembles a message into a bytestring representing a packet sent from this connection. */ + protected def assemble(message: Message): ByteString = { + val (messageId, payload) = Message.pack(message) + val packet: Packet = assembler.assemble(messageId, payload) + ByteString(packet.toArray) + } + + /** Parser for messages being sent to the uav. */ + protected val outgoing: Parser = new Parser(packet => Message.unpack(packet.messageId, packet.payload) match { + //TODO handle ping + /* + case Ping(`systemId`, `componentId`) => + val message = Ack(packet.systemId, packet.componentId) + val data = assemble(message) + self ! Connection.Received(data)*/ + case _ => () + }) + + /** Parser for messages coming from the uav. */ + protected val incoming: Parser = new Parser(pckt => + log.debug("incoming message: " + Message.unpack(pckt.messageId, pckt.payload))) + +} diff --git a/mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala b/mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala deleted file mode 100644 index 5e48ea1..0000000 --- a/mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala +++ /dev/null @@ -1,20 +0,0 @@ -package mavigator -package uav - -import akka.stream.scaladsl._ -import akka.stream._ -import org.reactivestreams._ - -private[uav] class Multiplexer[In, Out](service: Flow[In, Out, _])(implicit materializer: Materializer) { - - private val endpoint: Flow[Out, In, (Publisher[Out], Subscriber[In])] = Flow.fromSinkAndSourceMat( - Sink.asPublisher[Out](fanout = true), - Source.asSubscriber[In])((pub, sub) => (pub, sub)) - - private lazy val (publisher, subscriber) = (service.joinMat(endpoint)(Keep.right)).run() - - def connect(client: Flow[Out, In, _]) = { - Source.fromPublisher(publisher).via(client).to(Sink.ignore).run() - } - -} diff --git a/mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala.disabled b/mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala.disabled new file mode 100644 index 0000000..5e48ea1 --- /dev/null +++ b/mavigator-uav/src/main/scala/mavigator/uav/Multiplexer.scala.disabled @@ -0,0 +1,20 @@ +package mavigator +package uav + +import akka.stream.scaladsl._ +import akka.stream._ +import org.reactivestreams._ + +private[uav] class Multiplexer[In, Out](service: Flow[In, Out, _])(implicit materializer: Materializer) { + + private val endpoint: Flow[Out, In, (Publisher[Out], Subscriber[In])] = Flow.fromSinkAndSourceMat( + Sink.asPublisher[Out](fanout = true), + Source.asSubscriber[In])((pub, sub) => (pub, sub)) + + private lazy val (publisher, subscriber) = (service.joinMat(endpoint)(Keep.right)).run() + + def connect(client: Flow[Out, In, _]) = { + Source.fromPublisher(publisher).via(client).to(Sink.ignore).run() + } + +} diff --git a/mavigator-uav/src/main/scala/mavigator/uav/Uav.scala b/mavigator-uav/src/main/scala/mavigator/uav/Uav.scala index 06a8f00..8f8c083 100644 --- a/mavigator-uav/src/main/scala/mavigator/uav/Uav.scala +++ b/mavigator-uav/src/main/scala/mavigator/uav/Uav.scala @@ -32,7 +32,7 @@ class Uav(system: ExtendedActorSystem) extends Extension { def connect(): Flow[ByteString, ByteString, NotUsed] = { Flow.fromSinkAndSource( Sink.ignore, - (new MockConnection(0,0,1)).data + (new MockConnection(0,0,1)).data //TODO: use source instead of hardcoded value ) } diff --git a/mavigator-uav/src/main/scala/mavigator/uav/mock/RandomFlightPlan.scala b/mavigator-uav/src/main/scala/mavigator/uav/mock/RandomFlightPlan.scala index 89e5a75..680de1e 100644 --- a/mavigator-uav/src/main/scala/mavigator/uav/mock/RandomFlightPlan.scala +++ b/mavigator-uav/src/main/scala/mavigator/uav/mock/RandomFlightPlan.scala @@ -54,9 +54,10 @@ class RandomFlightPlan { 0 ) + def attitude = Attitude( millis, - (2 * math.Pi * time / 6).toFloat, + (math.sin(2 * math.Pi * time ) * math.Pi / 6).toFloat, (math.sin(2 * math.Pi * time / 5) * math.Pi / 6).toFloat, (2 * math.Pi * time / 4).toFloat, 0, diff --git a/project/Dependencies.scala b/project/Dependencies.scala index be6cbfe..ba39c94 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -19,8 +19,8 @@ object Dependencies { val flowNative = "com.github.jodersky" % "flow-native" % FlowVersion % Runtime val flowStream = "com.github.jodersky" % "flow-stream" % FlowVersion - val jsDom = Def.setting{"org.scala-js" %%% "scalajs-dom" % "0.8.2"} + val jsDom = Def.setting{"org.scala-js" %%% "scalajs-dom" % "0.9.0"} val scalatags = Def.setting{"com.lihaoyi" %%% "scalatags" % "0.5.4"} - val scalarx = Def.setting{"com.lihaoyi" %%% "scalarx" % "0.2.8"} + val scalarx = Def.setting{"com.lihaoyi" %%% "scalarx" % "0.3.1"} } diff --git a/project/Js.scala b/project/Js.scala index d665eab..9f85f3b 100644 --- a/project/Js.scala +++ b/project/Js.scala @@ -8,7 +8,7 @@ object Js { def dependsOnJs(proj: Project): Seq[Setting[_]] = Seq( resourceGenerators in Compile += Def.task{ - val js: File = (fullOptJS in (proj, Compile)).value.data + val js: File = (fastOptJS in (proj, Compile)).value.data val map = js.getParentFile / (js.name + ".map") val out = (resourceManaged in Compile).value / "assets" / "js" diff --git a/project/plugins.sbt b/project/plugins.sbt index 798db0c..301572f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,19 +2,18 @@ * Additional resolvers */ -resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" +//resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" -resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" +//resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" resolvers += Resolver.jcenterRepo - /* * Main plugins */ // add support for scalajs -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.6") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.8") // twirl html templating addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.1.1") -- cgit v1.2.3