diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..0a3f3dc
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,148 @@
+
+
+ 4.0.0
+ uk.org.floop.msc
+ weather
+ 1.0-SNAPSHOT
+ war
+ weather
+ 2007
+
+ 2.7.1
+
+
+
+
+ scala-tools.org
+ Scala-Tools Maven2 Repository
+ http://scala-tools.org/repo-releases
+
+
+
+
+
+ scala-tools.org
+ Scala-Tools Maven2 Repository
+ http://scala-tools.org/repo-releases
+
+
+
+
+
+ org.scala-lang
+ scala-library
+ ${scala.version}
+
+
+ net.liftweb
+ lift-webkit
+ 0.9
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+ junit
+ junit
+ 3.8.1
+ test
+
+
+ org.mortbay.jetty
+ jetty
+ [6.1.6,)
+ test
+
+
+
+ org.scala-lang
+ scala-compiler
+ ${scala.version}
+ test
+
+
+ de.huxhorn.lilith
+ de.huxhorn.lilith.3rdparty.rrd4j
+ 2.0.5
+
+
+
+
+ msc-weather
+ src/main/scala
+ src/test/scala
+
+
+ org.scala-tools
+ maven-scala-plugin
+
+
+
+ compile
+ testCompile
+
+
+
+
+ ${scala.version}
+
+
+
+ org.mortbay.jetty
+ maven-jetty-plugin
+
+ /
+ 5
+
+
+
+ net.sf.alchim
+ yuicompressor-maven-plugin
+
+
+
+ compress
+
+
+
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-eclipse-plugin
+
+ true
+
+ org.scala-lang:scala-library
+
+
+ ch.epfl.lamp.sdt.launching.SCALA_CONTAINER
+
+
+ ch.epfl.lamp.sdt.core.scalanature
+ org.eclipse.jdt.core.javanature
+
+
+ ch.epfl.lamp.sdt.core.scalabuilder
+
+
+
+
+
+
+
+
+ org.scala-tools
+ maven-scala-plugin
+
+ ${scala.version}
+
+
+
+
+
diff --git a/src/main/resources/barometer.xml b/src/main/resources/barometer.xml
new file mode 100644
index 0000000..9f9521b
--- /dev/null
+++ b/src/main/resources/barometer.xml
@@ -0,0 +1,24 @@
+
+
+ Millibars
+
+
+
+ barometer
+ ${rrdFile}
+
+ AVERAGE
+
+
+ barometerMb
+ barometer,33.86,*
+
+
+
+
+ barometerMb
+ #FF0000
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/rain.xml b/src/main/resources/rain.xml
new file mode 100644
index 0000000..dba12d8
--- /dev/null
+++ b/src/main/resources/rain.xml
@@ -0,0 +1,20 @@
+
+
+ in/hr
+
+
+
+ rainRate
+ ${rrdFile}
+
+ AVERAGE
+
+
+
+
+ rainRate
+ #FF0000
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/temp.xml b/src/main/resources/temp.xml
new file mode 100644
index 0000000..c7c9d70
--- /dev/null
+++ b/src/main/resources/temp.xml
@@ -0,0 +1,39 @@
+
+
+ Degrees C
+
+
+
+ inTemp
+ ${rrdFile}
+
+ AVERAGE
+
+
+ inTempC
+ inTemp,32,-,9,/,5,*
+
+
+ outTemp
+ ${rrdFile}
+
+ AVERAGE
+
+
+ outTempC
+ outTemp,32,-,9,/,5,*
+
+
+
+
+ inTempC
+ #FF0000
+
+
+
+ outTempC
+ #0000FF
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/tempBounds.xml b/src/main/resources/tempBounds.xml
new file mode 100644
index 0000000..68b1ba2
--- /dev/null
+++ b/src/main/resources/tempBounds.xml
@@ -0,0 +1,39 @@
+
+
+ Degrees C
+
+
+
+ inTempMin
+ ${rrdFile}
+
+ MIN
+
+
+ inTempMinC
+ inTempMin,32,-,9,/,5,*
+
+
+ outTempMin
+ ${rrdFile}
+
+ MIN
+
+
+ outTempMinC
+ outTempMin,32,-,9,/,5,*
+
+
+
+
+ inTempMinC
+ #FF0000
+
+
+
+ outTempMinC
+ #0000FF
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/wind.xml b/src/main/resources/wind.xml
new file mode 100644
index 0000000..98323d8
--- /dev/null
+++ b/src/main/resources/wind.xml
@@ -0,0 +1,39 @@
+
+
+ Knots
+
+
+
+ windSpeed
+ ${rrdFile}
+
+ AVERAGE
+
+
+ windSpeedKnots
+ windSpeed,0.868976242,*
+
+
+ windGust
+ ${rrdFile}
+
+ MAX
+
+
+ windGustKnots
+ windGust,0.868976242,*
+
+
+
+
+ windSpeedKnots
+ #FF0000
+
+
+
+ windGustKnots
+ #0000FF
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/windDir.xml b/src/main/resources/windDir.xml
new file mode 100644
index 0000000..01d83fb
--- /dev/null
+++ b/src/main/resources/windDir.xml
@@ -0,0 +1,20 @@
+
+
+ Degrees from North
+
+
+
+ windDir
+ ${rrdFile}
+
+ AVERAGE
+
+
+
+
+ windDir
+ #FF0000
+
+
+
+
\ No newline at end of file
diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala
new file mode 100644
index 0000000..bf90104
--- /dev/null
+++ b/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -0,0 +1,35 @@
+package bootstrap.liftweb
+
+import net.liftweb.util._
+import net.liftweb.http._
+import net.liftweb.sitemap._
+import net.liftweb.sitemap.Loc._
+import Helpers._
+
+import uk.org.floop.msc.wview.DataCollector
+import uk.org.floop.msc.rest.Graph
+
+/**
+ * A class that's instantiated early and run. It allows the application
+ * to modify lift's environment
+ */
+class Boot {
+ def boot {
+ // where to search snippet
+ LiftRules.addToPackages("uk.org.floop.msc")
+
+ val apiDispatcher: LiftRules.DispatchPf = {
+ case RequestMatcher(RequestState("graph" :: args, _) ,_) => graphApi(args)
+ }
+ LiftRules.statelessDispatchTable =
+ apiDispatcher orElse LiftRules.statelessDispatchTable
+ DataCollector.start()
+ }
+
+ private def graphApi
+ (args: List[String])
+ (req: RequestState): Can[ResponseIt] =
+ Graph(args)
+
+}
+
diff --git a/src/main/scala/uk/org/floop/msc/comet/.keep b/src/main/scala/uk/org/floop/msc/comet/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/comet/.keep
diff --git a/src/main/scala/uk/org/floop/msc/comet/WeatherActor.scala b/src/main/scala/uk/org/floop/msc/comet/WeatherActor.scala
new file mode 100644
index 0000000..e97c5b1
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/comet/WeatherActor.scala
@@ -0,0 +1,50 @@
+package uk.org.floop.msc.comet
+
+import scala.collection.mutable.HashMap
+
+import net.liftweb.http._
+import net.liftweb.http.js.JsCmds._
+
+import uk.org.floop.msc.rrd._
+
+class WeatherActor(info: CometActorInitInfo) extends CometActor(info) {
+
+ def defaultPrefix = "weather"
+
+ var currentWeather: List[Pair[String, Any]] = Nil
+ val currentWeatherMap = new HashMap[String, Any]()
+
+ def render: RenderOut = {
+ new RenderOut(bind("view" ->
+
+ {currentWeather.map(pair =>
+ {pair._1} | {pair._2} |
+ )}
+
),
+ currentWeatherMap.get("windDir") match {
+ case Some(angle) => Run("drawCompass(" + angle + ")")
+ case None => Noop
+ }
+ )
+ }
+
+ override def localSetup {
+ DataStore !? AddWeatherListener(this) match {
+ case CurrentWeather(w) =>
+ currentWeather = w
+ w.foreach(pair => currentWeatherMap(pair._1) = pair._2)
+ }
+ }
+
+ override def localShutdown {
+ DataStore ! RemoveWeatherListener(this)
+ }
+
+ override def lowPriority: PartialFunction[Any, Unit] = {
+ case CurrentWeather(w) =>
+ val diff = w -- currentWeather
+ diff.foreach(pair => currentWeatherMap(pair._1) = pair._2)
+ currentWeather = w
+ reRender(false)
+ }
+}
diff --git a/src/main/scala/uk/org/floop/msc/model/.keep b/src/main/scala/uk/org/floop/msc/model/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/model/.keep
diff --git a/src/main/scala/uk/org/floop/msc/rest/Graph.scala b/src/main/scala/uk/org/floop/msc/rest/Graph.scala
new file mode 100644
index 0000000..b9c849b
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/rest/Graph.scala
@@ -0,0 +1,41 @@
+package uk.org.floop.msc.rest
+
+import net.liftweb.http._
+import net.liftweb.util._
+
+
+import uk.org.floop.msc.rrd._
+
+object Graph {
+
+ def apply(args: List[String]): Can[ResponseIt] = {
+ args match {
+ case arg1 :: arg2 :: Nil => {
+ val timePeriod = arg1 match {
+ case "hour" => Some(TimePeriod.HOUR)
+ case "day" => Some(TimePeriod.DAY)
+ case "week" => Some(TimePeriod.WEEK)
+ case "month" => Some(TimePeriod.MONTH)
+ case "year" => Some(TimePeriod.YEAR)
+ case _ => None
+ }
+ timePeriod match {
+ case Some(p) =>
+ DataStore !? GenerateGraph("/" + arg2 + ".xml", p) match {
+ case Some(ImageGenerated(bytes)) => Full(Response(bytes, List(("Content-Type", "image/png")), Nil, 200))
+ case None =>
+ println("Unable to generate graph.")
+ Empty
+ }
+ case _ =>
+ println("Unrecognised time period.")
+ Empty
+ }
+ }
+ case _ =>
+ println("Undefined graph type or period.")
+ Empty
+ }
+ }
+
+}
diff --git a/src/main/scala/uk/org/floop/msc/rrd/DataStore.scala b/src/main/scala/uk/org/floop/msc/rrd/DataStore.scala
new file mode 100644
index 0000000..279fa90
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/rrd/DataStore.scala
@@ -0,0 +1,180 @@
+package uk.org.floop.msc.rrd
+
+import scala.actors.Actor
+import scala.actors.Actor._
+import scala.collection.mutable.ListBuffer
+
+import java.io.{File, IOException, ByteArrayOutputStream, InputStream}
+import java.util.{Date, Calendar}
+import java.awt.Color
+import java.awt.image.BufferedImage
+import javax.imageio.stream.MemoryCacheImageOutputStream
+import javax.imageio.ImageIO
+import _root_.org.xml.sax.InputSource
+
+import _root_.org.rrd4j.core.{RrdDef, RrdDb}
+import _root_.org.rrd4j.{DsType, ConsolFun}
+import _root_.org.rrd4j.graph.{RrdGraphDef, RrdGraph, RrdGraphDefTemplate}
+
+import net.liftweb.http.LiftRules
+import net.liftweb.util.{Full, Empty, Can}
+
+object TimePeriod extends Enumeration {
+ val HOUR, DAY, WEEK, MONTH, YEAR = Value
+ def last(p: Value): Array[Long] = {
+ val t2 = Calendar.getInstance.getTimeInMillis / 1000
+ val t1 = t2 - (p match {
+ case HOUR => 60 * 60
+ case DAY => 60 * 60 * 24
+ case WEEK => 60 * 60 * 24 * 7
+ case MONTH => 60 * 60 * 24 * 7 * 4
+ case YEAR => 60 * 60 * 24 * 7 * 4 * 12
+ })
+ Array(t1, t2)
+ }
+}
+
+case class StorePacket(p: List[Pair[String, Any]])
+case class GenerateGraph(template: String, p: TimePeriod.Value)
+case class ImageGenerated(bytes: Array[Byte])
+case class AddWeatherListener(l: Actor)
+case class RemoveWeatherListener(l: Actor)
+case class CurrentWeather(w: List[Pair[String, Any]])
+
+object DataStore extends Actor {
+
+ val STORE = new File("/usr/local/weather/weather.rrd")
+ val DS_LIST: List[Triple[String, Double, Double]] = List(
+ ("barometer", 26, 32),
+ ("stationPressure", 26, 32),
+ ("altimeter", 26, 32),
+ ("inTemp", 32, 140),
+ ("outTemp", -40, 150),
+ ("inHumidity", 0, 100),
+ ("outHumidity", 0, 100),
+ ("windSpeed", 0, 150),
+ ("windDir", 0, 360),
+ ("windGust", 0, 150),
+ ("windGustDir", 0, 360),
+ ("rainRate", 0, 100),
+ ("sampleRain", 0, 100),
+ ("sampleET", Double.NaN, Double.NaN),
+ ("radiation", Double.NaN, Double.NaN),
+ ("UV", Double.NaN, Double.NaN),
+ ("dewpoint", -105, 130),
+ ("windchill", -120, 130),
+ ("heatindex", -40, 135),
+ // skip computed fields
+ ("rxCheckPercent", 0, 100),
+ // skip forecasts
+ ("txBatteryStatus", Double.NaN, Double.NaN),
+ ("consBatteryVoltage", Double.NaN, Double.NaN)
+ )
+ if (!STORE.exists) {
+ val rrdDef = new RrdDef(STORE.getPath)
+ rrdDef.setStartTime(new Date())
+ rrdDef.setStep(15)
+ DS_LIST.foreach( triple =>
+ rrdDef.addDatasource(triple._1, DsType.GAUGE, 300, triple._2, triple._3)
+ )
+ List(ConsolFun.AVERAGE, ConsolFun.MAX, ConsolFun.MIN).foreach( fun => {
+ rrdDef.addArchive(fun, 0.5, 1, 240) // 15 second consolidated, hour stored
+ rrdDef.addArchive(fun, 0.5, 60, 24*4) // 15 minute consolidated, day stored
+ rrdDef.addArchive(fun, 0.5, 240, 24*7) // Hour consolidated, week stored
+ rrdDef.addArchive(fun, 0.5, 240*24, 28) // Day consolidated, month stored
+ rrdDef.addArchive(fun, 0.5, 240*24*7, 52) // Week consolidated, year stored
+ })
+ val rrdb = new RrdDb(rrdDef)
+ rrdb.close()
+ }
+
+ var currentWeather: List[Pair[String, Any]] = Nil
+ val listeners = new ListBuffer[Actor]
+
+ def act() {
+ loop {
+ react {
+ case AddWeatherListener(l) =>
+ listeners += l
+ reply(CurrentWeather(currentWeather))
+ case RemoveWeatherListener(l) =>
+ listeners -= l
+ case StorePacket(p) =>
+ currentWeather = p
+ try {
+ val rrdb = new RrdDb(STORE.getPath)
+ try {
+ val sample = rrdb.createSample()
+ p.foreach( fieldValue => {
+ if (rrdb.containsDs(fieldValue._1)) {
+ sample.setValue(fieldValue._1, fieldValue._2 match {
+ case n: Number => n.doubleValue
+ case _ => Double.NaN
+ })
+ }
+ })
+ sample.update()
+ } catch {
+ case ex: IOException => println("StorePacket, IOException: " + ex.toString)
+ case ex: IllegalArgumentException => println("StorePacket, IllegalArgumentException: " + ex.toString)
+ } finally {
+ rrdb.close()
+ }
+ } catch {
+ case ex: IOException => println("StorePacket, IOException: " + ex.toString)
+ }
+ updateListeners
+ case GenerateGraph(t, p) =>
+ val gd = LiftRules.getResourceAsStream(t) match {
+ case Full(inputStream) =>
+ try {
+ val templ = new RrdGraphDefTemplate(new InputSource(inputStream))
+ templ.setVariable("rrdFile", STORE.getPath)
+ Some(templ.getRrdGraphDef())
+ } catch {
+ case ex: IOException =>
+ println(ex)
+ None
+ case ex: IllegalArgumentException =>
+ println(ex)
+ None
+ }
+ case x =>
+ println(x)
+ None
+ }
+ gd match {
+ case Some(gd) =>
+ gd.setTimeSpan(TimePeriod.last(p))
+ gd.setFilename("-") // in memory only
+ gd.setImageFormat("PNG")
+ gd.setAntiAliasing(true)
+ reply(
+ try {
+ val g = new RrdGraph(gd)
+ val gi = g.getRrdGraphInfo()
+ val bi = new BufferedImage(gi.getWidth, gi.getHeight, BufferedImage.TYPE_INT_ARGB)
+ g.render(bi.getGraphics)
+ val ba = new ByteArrayOutputStream()
+ ImageIO.write(bi, "PNG", new MemoryCacheImageOutputStream(ba))
+ Some(ImageGenerated(ba.toByteArray))
+ } catch {
+ case ex: Exception =>
+ println(ex)
+ None
+ }
+ )
+ case None =>
+ reply(None)
+ }
+ }
+ }
+ }
+
+ private def updateListeners {
+ listeners.foreach( _ ! CurrentWeather(currentWeather) )
+ }
+
+ start()
+
+}
diff --git a/src/main/scala/uk/org/floop/msc/snippet/.keep b/src/main/scala/uk/org/floop/msc/snippet/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/snippet/.keep
diff --git a/src/main/scala/uk/org/floop/msc/snippet/HelloWorld.scala b/src/main/scala/uk/org/floop/msc/snippet/HelloWorld.scala
new file mode 100644
index 0000000..f25066c
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/snippet/HelloWorld.scala
@@ -0,0 +1,6 @@
+package uk.org.floop.msc.snippet
+
+class HelloWorld {
+ def howdy = Welcome to weather at {new java.util.Date}
+}
+
diff --git a/src/main/scala/uk/org/floop/msc/view/.keep b/src/main/scala/uk/org/floop/msc/view/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/view/.keep
diff --git a/src/main/scala/uk/org/floop/msc/wview/DataCollector.scala b/src/main/scala/uk/org/floop/msc/wview/DataCollector.scala
new file mode 100644
index 0000000..d1f23d3
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/wview/DataCollector.scala
@@ -0,0 +1,99 @@
+package uk.org.floop.msc.wview
+
+import scala.actors.Actor
+import scala.collection.mutable.ListBuffer
+
+import java.net.{InetSocketAddress, SocketTimeoutException}
+import java.io.IOException
+import java.nio.channels.{SocketChannel, ClosedByInterruptException}
+import java.nio.ByteBuffer
+import java.util.Date
+
+import uk.org.floop.msc.rrd._
+
+object DataCollector extends Actor {
+
+ def act() {
+ var continue = true
+ var holdoff = 1000
+ while (continue) {
+ var sc: SocketChannel = null
+ try {
+ var startFramePos = 0
+ var readPos = 0
+ var packetPos = 0
+ val values = new ListBuffer[Pair[String, Any]]()
+
+ val sa = new InetSocketAddress("10.79.0.6", 11011)
+ sc = SocketChannel.open()
+ sc.socket.setSoTimeout(30000)
+ sc.connect(sa)
+ val bb = ByteBuffer.allocate(4096)
+
+ while (sc.read(bb) > 0) {
+ holdoff = 1000
+ if (startFramePos < 8) { // still reading start frame
+ while ((startFramePos < 8) && (readPos < bb.position)) {
+ if (bb.get(readPos) == LoopPacket.START_FRAME(startFramePos)) {
+ readPos += 1
+ startFramePos += 1
+ } else { // start frame doesn't match; rewind
+ readPos = readPos - startFramePos + 1
+ startFramePos = 0
+ }
+ }
+ }
+ if (startFramePos == 8) {
+ while ((packetPos < LoopPacket.fields.length) &&
+ (readPos <= (bb.position - VALUE_TYPE.sizeof(LoopPacket.fields(packetPos)._1)))) {
+ LoopPacket.fields(packetPos) match {
+ case (VALUE_TYPE.FLOAT, field) => values += (field, bb.getFloat(readPos))
+ case (VALUE_TYPE.USHORT, field) => values += (field, bb.getShort(readPos))
+ case (VALUE_TYPE.TIME_T, field) => values += (field, bb.getInt(readPos) match {
+ case 0 => None
+ case x => Some(new Date(x.toLong * 1000))
+ })
+ case (VALUE_TYPE.SHORT, field) => values += (field, bb.getShort(readPos))
+ case (VALUE_TYPE.UCHAR, field) => values += (field, bb.get(readPos))
+ }
+ readPos += VALUE_TYPE.sizeof(LoopPacket.fields(packetPos)._1)
+ packetPos += 1
+ }
+ if (packetPos == LoopPacket.fields.length) { // read entire loop packet
+ DataStore ! StorePacket(values.toList)
+ values.clear()
+ packetPos = 0
+ startFramePos = 0
+ if (readPos == bb.position) { // just reset the buffer
+ bb.clear()
+ } else { // move remaining bytes back to start
+ System.arraycopy(bb.array, readPos, bb.array, 0, bb.position - readPos)
+ bb.position(bb.position - readPos)
+ }
+ readPos = 0
+ }
+ }
+ }
+ println("DataCollector: empty read, restarting.")
+ Thread.sleep(holdoff)
+ if (holdoff < 60000) {
+ holdoff = holdoff * 2
+ }
+ } catch {
+ case ex: ClosedByInterruptException =>
+ continue = false // some other thread interrupted us, so don't continue
+ println("DataCollector interrupted, closing down.")
+ case ex: IOException => // includes SocketTimeoutException
+ // could occur in sc.open or sc.read, either way, close up but continue (re-open, etc.)
+ println("DataCollector exception: " + ex)
+ Thread.sleep(holdoff)
+ if (holdoff < 60000) {
+ holdoff = holdoff * 2
+ }
+ } finally {
+ sc.close()
+ }
+ }
+ }
+
+}
diff --git a/src/main/scala/uk/org/floop/msc/wview/LoopPacket.scala b/src/main/scala/uk/org/floop/msc/wview/LoopPacket.scala
new file mode 100644
index 0000000..71c56cc
--- /dev/null
+++ b/src/main/scala/uk/org/floop/msc/wview/LoopPacket.scala
@@ -0,0 +1,86 @@
+package uk.org.floop.msc.wview
+
+object VALUE_TYPE extends Enumeration {
+ val FLOAT, USHORT, TIME_T, SHORT, UCHAR = Value
+ def sizeof(t: Value): Int = t match {
+ case VALUE_TYPE.FLOAT => 4
+ case VALUE_TYPE.USHORT => 2
+ case VALUE_TYPE.TIME_T => 4
+ case VALUE_TYPE.SHORT => 2
+ case VALUE_TYPE.UCHAR => 1
+ }
+}
+
+object LoopPacket {
+ val fields = List(
+ (VALUE_TYPE.FLOAT, "barometer"), // inches
+ (VALUE_TYPE.FLOAT, "stationPressure"), // inches
+ (VALUE_TYPE.FLOAT, "altimeter"), // inches
+ (VALUE_TYPE.FLOAT, "inTemp"), // degrees F
+ (VALUE_TYPE.FLOAT, "outTemp"), // degrees F
+ (VALUE_TYPE.USHORT, "inHumidity"), // percent
+ (VALUE_TYPE.USHORT, "outHumidity"), // percent
+ (VALUE_TYPE.USHORT, "windSpeed"), // mph
+ (VALUE_TYPE.USHORT, "windDir"), // degrees
+ (VALUE_TYPE.USHORT, "windGust"), // mph
+ (VALUE_TYPE.USHORT, "windGustDir"), // degrees
+ (VALUE_TYPE.FLOAT, "rainRate"), // in/hr
+ (VALUE_TYPE.FLOAT, "sampleRain"), // inches
+ (VALUE_TYPE.FLOAT, "sampleET"), // ET
+ (VALUE_TYPE.USHORT, "radiation"), // watts/m^3
+ (VALUE_TYPE.USHORT, "UV"), // UV index * 10
+ (VALUE_TYPE.FLOAT, "dewpoint"), // degrees F
+ (VALUE_TYPE.FLOAT, "windchill"), // degrees F
+ (VALUE_TYPE.FLOAT, "heatindex"), // degrees F
+
+ // computed values - station should not alter these
+ (VALUE_TYPE.FLOAT, "stormRain"), // inches
+ (VALUE_TYPE.TIME_T, "stormStart"), // time_t
+ (VALUE_TYPE.FLOAT, "dayRain"), // inches
+ (VALUE_TYPE.FLOAT, "monthRain"), // inches
+ (VALUE_TYPE.FLOAT, "yearRain"), // inches
+ (VALUE_TYPE.FLOAT, "dayET"), // inches
+ (VALUE_TYPE.FLOAT, "monthET"), // inches
+ (VALUE_TYPE.FLOAT, "yearET"), // inches
+ (VALUE_TYPE.FLOAT, "intervalAvgWCHILL"), // degrees F
+ (VALUE_TYPE.SHORT, "intervalAvgWSPEED"), // mph
+ (VALUE_TYPE.USHORT, "yearRainMonth"), // 1-12 Rain Start Month
+
+ // --- The following may or may not be supported for a given station ---
+
+ // Vantage Pro
+ (VALUE_TYPE.USHORT, "rxCheckPercent"), // 0 - 100
+ (VALUE_TYPE.USHORT, "tenMinuteAvgWindSpeed"), // mph
+ (VALUE_TYPE.USHORT, "forecastIcon"), // VP only
+ (VALUE_TYPE.USHORT, "forecastRule"), // VP only
+ (VALUE_TYPE.USHORT, "txBatteryStatus"), // VP only
+ (VALUE_TYPE.USHORT, "consBatteryVoltage"), // VP only
+ (VALUE_TYPE.UCHAR, "extraTemp1"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "extraTemp2"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "extraTemp3"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "soilTemp1"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "soilTemp2"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "soilTemp3"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "soilTemp4"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "leafTemp1"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "leafTemp2"), // degrees F + 90
+ (VALUE_TYPE.UCHAR, "extraHumid1"), // percent
+ (VALUE_TYPE.UCHAR, "extraHumid2"), // percent
+ (VALUE_TYPE.UCHAR, "soilMoist1"),
+ (VALUE_TYPE.UCHAR, "soilMoist2"),
+ (VALUE_TYPE.UCHAR, "leafWet1"),
+ (VALUE_TYPE.UCHAR, "leafWet2"),
+ (VALUE_TYPE.UCHAR, "pad1"),
+
+ // Vaisala WXT-510
+ (VALUE_TYPE.FLOAT, "wxt510Hail"), // inches
+ (VALUE_TYPE.FLOAT, "wxt510Hailrate"), // in/hr
+ (VALUE_TYPE.FLOAT, "wxt510HeatingTemp"), // degrees F
+ (VALUE_TYPE.FLOAT, "wxt510HeatingVoltage"), // volts
+ (VALUE_TYPE.FLOAT, "wxt510SupplyVoltage"), // volts
+ (VALUE_TYPE.FLOAT, "wxt510ReferenceVoltage") // volts
+ )
+
+ val START_FRAME = Array(0xf3, 0x88, 0xc6, 0xa2, 0xda, 0xda, 0xe7, 0xcf).map(_.toByte)
+
+}
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..a434743
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ LiftFilter
+ Lift Filter
+ The Filter that intercepts lift calls
+ net.liftweb.http.LiftFilter
+
+
+
+
+ LiftFilter
+ /*
+
+
+
diff --git a/src/main/webapp/charts.js b/src/main/webapp/charts.js
new file mode 100644
index 0000000..8b5edea
--- /dev/null
+++ b/src/main/webapp/charts.js
@@ -0,0 +1,187 @@
+var cCanvas, cCtx, ressie_angle = 0;
+var CIRCLE_RADIUS = 0.75;
+var REAR_ARROW_WIDTH = 0.18;
+var FRONT_ARROW_WIDTH = 0.15;
+var BEARING_ARROW_WIDTH = 0.08;
+var BEARING_ARROW_LENGTH = 0.65;
+
+function drawCompass(angle) {
+ cCanvas = document.getElementById("compass");
+ if (!cCanvas) {
+ return;
+ }
+ cCtx = cCanvas.getContext("2d");
+ if (!cCtx) {
+ return;
+ }
+ cCtx.clearRect(0, 0, cCanvas.width, cCanvas.height);
+ var minWidthHeight = Math.min(cCanvas.width, cCanvas.height);
+ var FRONT_45 = minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4) * FRONT_ARROW_WIDTH;
+ cCtx.save();
+ cCtx.translate(cCanvas.width / 2, cCanvas.height / 2);
+ cCtx.strokeStyle = "rgb(136, 136, 136)";
+ cCtx.lineWidth = 0.75;
+ for (var i = 0; i < 2; i++) {
+ cCtx.beginPath();
+ cCtx.arc(0, 0, minWidthHeight / 2 * CIRCLE_RADIUS, 0, Math.PI * 2, true);
+ cCtx.closePath();
+ if (i == 0) {
+ cCtx.fillStyle = "rgba(255,255,255,0.5)";
+ cCtx.fill();
+ } else {
+ cCtx.stroke();
+ }
+ }
+ for (var i = 0; i < 2; i++) {
+ cCtx.beginPath();
+ cCtx.moveTo(0, -minWidthHeight / 2 * REAR_ARROW_WIDTH);
+ cCtx.lineTo(minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4),
+ -minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4));
+ cCtx.lineTo(minWidthHeight / 2 * REAR_ARROW_WIDTH, 0);
+ cCtx.lineTo(minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4),
+ minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4));
+ cCtx.lineTo(0, minWidthHeight / 2 * REAR_ARROW_WIDTH);
+ cCtx.lineTo(-minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4),
+ minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4));
+ cCtx.lineTo(-minWidthHeight / 2 * REAR_ARROW_WIDTH, 0);
+ cCtx.lineTo(-minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4),
+ -minWidthHeight / 2 * CIRCLE_RADIUS * Math.sin(Math.PI / 4));
+ cCtx.lineTo(0, -minWidthHeight / 2 * REAR_ARROW_WIDTH);
+ cCtx.closePath();
+ if (i == 0) {
+ cCtx.fillStyle = "rgb(0, 0, 255)";
+ cCtx.fill();
+ } else {
+ cCtx.stroke();
+ }
+ }
+ for (var i = 0; i < 2; i++) {
+ cCtx.save();
+ for(var j=0; j<4; j++) {
+ cCtx.beginPath();
+ cCtx.moveTo(0, 0);
+ cCtx.lineTo(FRONT_45, -FRONT_45);
+ cCtx.lineTo(0, -minWidthHeight / 2);
+ cCtx.lineTo(0, 0);
+ cCtx.closePath();
+ if (i == 0) {
+ cCtx.fillStyle = "rgb(255,0,0)";
+ cCtx.fill();
+ } else {
+ cCtx.stroke();
+ }
+ cCtx.beginPath();
+ cCtx.moveTo(0, 0);
+ cCtx.lineTo(-FRONT_45, -FRONT_45);
+ cCtx.lineTo(0, -minWidthHeight / 2);
+ cCtx.lineTo(0, 0);
+ cCtx.closePath();
+ if (i == 0) {
+ cCtx.fillStyle = "rgb(255,255,255)";
+ cCtx.fill();
+ } else {
+ cCtx.stroke();
+ }
+ cCtx.rotate(Math.PI / 2);
+ }
+ cCtx.restore();
+ }
+ for (var i = 0; i < 2; i++) {
+ cCtx.save();
+ cCtx.rotate(angle * Math.PI / 180.0);
+ cCtx.beginPath();
+ cCtx.moveTo(0, BEARING_ARROW_WIDTH * minWidthHeight / 2);
+ cCtx.lineTo(BEARING_ARROW_WIDTH * minWidthHeight / 2, 0);
+ cCtx.lineTo(0, -minWidthHeight * BEARING_ARROW_LENGTH / 2);
+ cCtx.lineTo(-BEARING_ARROW_WIDTH * minWidthHeight / 2, 0);
+ cCtx.lineTo(0, BEARING_ARROW_WIDTH * minWidthHeight / 2);
+ cCtx.closePath();
+ if (i == 0) {
+ cCtx.fillStyle = "rgb(238,238,0)";
+ cCtx.fill();
+ } else {
+ cCtx.lineWidth = 3;
+ cCtx.strokeStyle = "rgb(0, 0, 0)";
+ cCtx.stroke();
+ }
+ cCtx.restore();
+ }
+ cCtx.restore();
+}
+
+function svgPathMap(svg_path, move_fun, line_fun, curve_fun, close_fun) {
+ var i = 0;
+ while (i < svg_path.length) {
+ if (svg_path[i] == "M") {
+ var move_xy = svg_path[i+1].split(",");
+ move_fun(move_xy[0], move_xy[1]);
+ i = i + 2;
+ } else if (svg_path[i] == "L") {
+ var line_xy = svg_path[i+1].split(",");
+ line_fun(line_xy[0], line_xy[1]);
+ i = i + 2;
+ } else if (svg_path[i] == "C") {
+ var xy1 = svg_path[i+1].split(","), xy2 = svg_path[i+2].split(","), xy3 = svg_path[i+3].split(",");
+ curve_fun(xy1[0], xy1[1], xy2[0], xy2[1], xy3[0], xy3[1]);
+ i = i + 4;
+ } else if (svg_path[i] == "z") {
+ close_fun();
+ i++;
+ }
+ }
+}
+
+function drawRessie() {
+ cCanvas = document.getElementById("compass");
+ if (!cCanvas) {
+ return;
+ }
+ cCtx = cCanvas.getContext("2d");
+ if (!cCtx) {
+ return;
+ }
+ cCtx.clearRect(0, 0, cCanvas.width, cCanvas.height);
+ var SVG_PATH=("M 242.14286,620.93361 C 242.14286,620.93361 307.14286,540.93361 318.57143,526.6479 "+
+ "C 330,512.36218 309.28571,498.07647 309.28571,498.07647 C 309.28571,498.07647 291.19688,481.65613 287.85714,477.36218 "+
+ "C 282.85715,470.93361 290.78801,462.50108 298.57143,463.79075 C 310.72728,465.80491 334.28571,463.79075 347.85714,456.6479 "+
+ "C 361.42857,449.50504 360.71429,440.21933 370,435.21933 C 379.28571,430.21933 390,425.93361 407.14286,423.07647 "+
+ "C 424.05247,420.2582 430.71429,413.07647 437.14286,409.50504 C 443.57143,405.93361 450,398.07647 451.42857,405.21933 "+
+ "C 452.85714,412.36218 480,520.93361 480,520.93361 C 480,520.93361 447.14286,551.6479 440,555.93361 "+
+ "C 432.85714,560.21933 427.85714,567.36218 423.57143,567.36218 C 419.28571,567.36218 409.28571,574.50504 409.28571,574.50504 "+
+ "L 408.57143,594.50504 C 408.57143,594.50504 407.85714,615.21933 405,615.93361 "+
+ "C 402.14286,616.6479 392.14286,625.93361 375,613.79075 C 357.85714,601.6479 337.14286,593.07647 327.14286,595.93361 "+
+ "C 317.14286,598.79075 298.57143,603.07647 298.57143,603.07647 C 298.57143,603.07647 265,641.6479 259.28571,641.6479 "+
+ "C 253.57143,641.6479 243.57143,643.07647 240.71429,635.93361 C 237.85714,628.79076 242.14286,621.6479 242.14286,620.93361 z").split(" ");
+ var max_x = 0, max_y = 0, min_x = 2000, min_y = 2000;
+ var minWidthHeight = Math.min(cCanvas.width, cCanvas.height);
+ function bound(x, y) {
+ max_x = Math.max(max_x, x);
+ max_y = Math.max(max_y, y);
+ min_x = Math.min(min_x, x);
+ min_y = Math.min(min_y, y);
+ }
+ svgPathMap(SVG_PATH, bound, bound, function(cx1, cy1, cx2, cy2, x, y) {bound(x, y);}, function() {});
+ cCtx.save();
+ if (ressie_angle < 360) {
+ ressie_angle = ressie_angle + 1;
+ } else {
+ ressie_angle = 0;
+ }
+ cCtx.translate(minWidthHeight / 2, minWidthHeight / 2);
+ cCtx.rotate(Math.PI / 180 * ressie_angle);
+ var scale = minWidthHeight / Math.max(max_x - min_x, max_y - min_y) / Math.sqrt(2);
+ cCtx.scale(scale, scale);
+ cCtx.translate(-(min_x + max_x) / 2, -(min_y + max_y) / 2);
+ cCtx.beginPath();
+ cCtx.strokeStyle = "rgb(136, 136, 136)";
+ cCtx.fillStyle = "rgb(34, 170, 238)";
+ svgPathMap(SVG_PATH, function(x, y) {cCtx.moveTo(x, y);},
+ function(x, y) {cCtx.lineTo(x, y);},
+ function(x1, y1, x2, y2, x, y) {cCtx.bezierCurveTo(x1, y1, x2, y2, x, y);},
+ function() {cCtx.closePath(); cCtx.fill();});
+ svgPathMap(SVG_PATH, function(x, y) {cCtx.moveTo(x, y);},
+ function(x, y) {cCtx.lineTo(x, y);},
+ function(x1, y1, x2, y2, x, y) {cCtx.bezierCurveTo(x1, y1, x2, y2, x, y);},
+ function() {cCtx.closePath(); cCtx.stroke();});
+ cCtx.restore();
+}
diff --git a/src/main/webapp/excanvas.js b/src/main/webapp/excanvas.js
new file mode 100644
index 0000000..3e1aedf
--- /dev/null
+++ b/src/main/webapp/excanvas.js
@@ -0,0 +1,785 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+// different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+// width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+// Quirks mode will draw the canvas using border-box. Either change your
+// doctype to HTML5
+// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+// or use Box Sizing Behavior from WebFX
+// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Optimize. There is always room for speed improvements.
+
+// only add this code if we do not already have a canvas implementation
+if (!window.CanvasRenderingContext2D) {
+
+(function () {
+
+ // alias some functions to make (compiled) code shorter
+ var m = Math;
+ var mr = m.round;
+ var ms = m.sin;
+ var mc = m.cos;
+
+ // this is used for sub pixel precision
+ var Z = 10;
+ var Z2 = Z / 2;
+
+ var G_vmlCanvasManager_ = {
+ init: function (opt_doc) {
+ var doc = opt_doc || document;
+ if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+ var self = this;
+ doc.attachEvent("onreadystatechange", function () {
+ self.init_(doc);
+ });
+ }
+ },
+
+ init_: function (doc) {
+ if (doc.readyState == "complete") {
+ // create xmlns
+ if (!doc.namespaces["g_vml_"]) {
+ doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml");
+ }
+
+ // setup default css
+ var ss = doc.createStyleSheet();
+ ss.cssText = "canvas{display:inline-block;overflow:hidden;" +
+ // default size is 300x150 in Gecko and Opera
+ "text-align:left;width:300px;height:150px}" +
+ "g_vml_\\:*{behavior:url(#default#VML)}";
+
+ // find all canvas elements
+ var els = doc.getElementsByTagName("canvas");
+ for (var i = 0; i < els.length; i++) {
+ if (!els[i].getContext) {
+ this.initElement(els[i]);
+ }
+ }
+ }
+ },
+
+ fixElement_: function (el) {
+ // in IE before version 5.5 we would need to add HTML: to the tag name
+ // but we do not care about IE before version 6
+ var outerHTML = el.outerHTML;
+
+ var newEl = el.ownerDocument.createElement(outerHTML);
+ // if the tag is still open IE has created the children as siblings and
+ // it has also created a tag with the name "/FOO"
+ if (outerHTML.slice(-2) != "/>") {
+ var tagName = "/" + el.tagName;
+ var ns;
+ // remove content
+ while ((ns = el.nextSibling) && ns.tagName != tagName) {
+ ns.removeNode();
+ }
+ // remove the incorrect closing tag
+ if (ns) {
+ ns.removeNode();
+ }
+ }
+ el.parentNode.replaceChild(newEl, el);
+ return newEl;
+ },
+
+ /**
+ * Public initializes a canvas element so that it can be used as canvas
+ * element from now on. This is called automatically before the page is
+ * loaded but if you are creating elements using createElement you need to
+ * make sure this is called on the element.
+ * @param {HTMLElement} el The canvas element to initialize.
+ * @return {HTMLElement} the element that was created.
+ */
+ initElement: function (el) {
+ el = this.fixElement_(el);
+ el.getContext = function () {
+ if (this.context_) {
+ return this.context_;
+ }
+ return this.context_ = new CanvasRenderingContext2D_(this);
+ };
+
+ // do not use inline function because that will leak memory
+ el.attachEvent('onpropertychange', onPropertyChange);
+ el.attachEvent('onresize', onResize);
+
+ var attrs = el.attributes;
+ if (attrs.width && attrs.width.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setWidth_(attrs.width.nodeValue);
+ el.style.width = attrs.width.nodeValue + "px";
+ } else {
+ el.width = el.clientWidth;
+ }
+ if (attrs.height && attrs.height.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setHeight_(attrs.height.nodeValue);
+ el.style.height = attrs.height.nodeValue + "px";
+ } else {
+ el.height = el.clientHeight;
+ }
+ //el.getContext().setCoordsize_()
+ return el;
+ }
+ };
+
+ function onPropertyChange(e) {
+ var el = e.srcElement;
+
+ switch (e.propertyName) {
+ case 'width':
+ el.style.width = el.attributes.width.nodeValue + "px";
+ el.getContext().clearRect();
+ break;
+ case 'height':
+ el.style.height = el.attributes.height.nodeValue + "px";
+ el.getContext().clearRect();
+ break;
+ }
+ }
+
+ function onResize(e) {
+ var el = e.srcElement;
+ if (el.firstChild) {
+ el.firstChild.style.width = el.clientWidth + 'px';
+ el.firstChild.style.height = el.clientHeight + 'px';
+ }
+ }
+
+ G_vmlCanvasManager_.init();
+
+ // precompute "00" to "FF"
+ var dec2hex = [];
+ for (var i = 0; i < 16; i++) {
+ for (var j = 0; j < 16; j++) {
+ dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
+ }
+ }
+
+ function createMatrixIdentity() {
+ return [
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1]
+ ];
+ }
+
+ function matrixMultiply(m1, m2) {
+ var result = createMatrixIdentity();
+
+ for (var x = 0; x < 3; x++) {
+ for (var y = 0; y < 3; y++) {
+ var sum = 0;
+
+ for (var z = 0; z < 3; z++) {
+ sum += m1[x][z] * m2[z][y];
+ }
+
+ result[x][y] = sum;
+ }
+ }
+ return result;
+ }
+
+ function copyState(o1, o2) {
+ o2.fillStyle = o1.fillStyle;
+ o2.lineCap = o1.lineCap;
+ o2.lineJoin = o1.lineJoin;
+ o2.lineWidth = o1.lineWidth;
+ o2.miterLimit = o1.miterLimit;
+ o2.shadowBlur = o1.shadowBlur;
+ o2.shadowColor = o1.shadowColor;
+ o2.shadowOffsetX = o1.shadowOffsetX;
+ o2.shadowOffsetY = o1.shadowOffsetY;
+ o2.strokeStyle = o1.strokeStyle;
+ o2.arcScaleX_ = o1.arcScaleX_;
+ o2.arcScaleY_ = o1.arcScaleY_;
+ }
+
+ function processStyle(styleString) {
+ var str, alpha = 1;
+
+ styleString = String(styleString);
+ if (styleString.substring(0, 3) == "rgb") {
+ var start = styleString.indexOf("(", 3);
+ var end = styleString.indexOf(")", start + 1);
+ var guts = styleString.substring(start + 1, end).split(",");
+
+ str = "#";
+ for (var i = 0; i < 3; i++) {
+ str += dec2hex[Number(guts[i])];
+ }
+
+ if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) {
+ alpha = guts[3];
+ }
+ } else {
+ str = styleString;
+ }
+
+ return [str, alpha];
+ }
+
+ function processLineCap(lineCap) {
+ switch (lineCap) {
+ case "butt":
+ return "flat";
+ case "round":
+ return "round";
+ case "square":
+ default:
+ return "square";
+ }
+ }
+
+ /**
+ * This class implements CanvasRenderingContext2D interface as described by
+ * the WHATWG.
+ * @param {HTMLElement} surfaceElement The element that the 2D context should
+ * be associated with
+ */
+ function CanvasRenderingContext2D_(surfaceElement) {
+ this.m_ = createMatrixIdentity();
+
+ this.mStack_ = [];
+ this.aStack_ = [];
+ this.currentPath_ = [];
+
+ // Canvas context properties
+ this.strokeStyle = "#000";
+ this.fillStyle = "#000";
+
+ this.lineWidth = 1;
+ this.lineJoin = "miter";
+ this.lineCap = "butt";
+ this.miterLimit = Z * 1;
+ this.globalAlpha = 1;
+ this.canvas = surfaceElement;
+
+ var el = surfaceElement.ownerDocument.createElement('div');
+ el.style.width = surfaceElement.clientWidth + 'px';
+ el.style.height = surfaceElement.clientHeight + 'px';
+ el.style.overflow = 'hidden';
+ el.style.position = 'absolute';
+ surfaceElement.appendChild(el);
+
+ this.element_ = el;
+ this.arcScaleX_ = 1;
+ this.arcScaleY_ = 1;
+ };
+
+ var contextPrototype = CanvasRenderingContext2D_.prototype;
+ contextPrototype.clearRect = function() {
+ this.element_.innerHTML = "";
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.beginPath = function() {
+ // TODO: Branch current matrix so that save/restore has no effect
+ // as per safari docs.
+
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.moveTo = function(aX, aY) {
+ this.currentPath_.push({type: "moveTo", x: aX, y: aY});
+ this.currentX_ = aX;
+ this.currentY_ = aY;
+ };
+
+ contextPrototype.lineTo = function(aX, aY) {
+ this.currentPath_.push({type: "lineTo", x: aX, y: aY});
+ this.currentX_ = aX;
+ this.currentY_ = aY;
+ };
+
+ contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+ aCP2x, aCP2y,
+ aX, aY) {
+ this.currentPath_.push({type: "bezierCurveTo",
+ cp1x: aCP1x,
+ cp1y: aCP1y,
+ cp2x: aCP2x,
+ cp2y: aCP2y,
+ x: aX,
+ y: aY});
+ this.currentX_ = aX;
+ this.currentY_ = aY;
+ };
+
+ contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+ // the following is lifted almost directly from
+ // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+ var cp1x = this.currentX_ + 2.0 / 3.0 * (aCPx - this.currentX_);
+ var cp1y = this.currentY_ + 2.0 / 3.0 * (aCPy - this.currentY_);
+ var cp2x = cp1x + (aX - this.currentX_) / 3.0;
+ var cp2y = cp1y + (aY - this.currentY_) / 3.0;
+ this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, aX, aY);
+ };
+
+ contextPrototype.arc = function(aX, aY, aRadius,
+ aStartAngle, aEndAngle, aClockwise) {
+ aRadius *= Z;
+ var arcType = aClockwise ? "at" : "wa";
+
+ var xStart = aX + (mc(aStartAngle) * aRadius) - Z2;
+ var yStart = aY + (ms(aStartAngle) * aRadius) - Z2;
+
+ var xEnd = aX + (mc(aEndAngle) * aRadius) - Z2;
+ var yEnd = aY + (ms(aEndAngle) * aRadius) - Z2;
+
+ // IE won't render arches drawn counter clockwise if xStart == xEnd.
+ if (xStart == xEnd && !aClockwise) {
+ xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+ // that can be represented in binary
+ }
+
+ this.currentPath_.push({type: arcType,
+ x: aX,
+ y: aY,
+ radius: aRadius,
+ xStart: xStart,
+ yStart: yStart,
+ xEnd: xEnd,
+ yEnd: yEnd});
+
+ };
+
+ contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ };
+
+ contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+ // Will destroy any existing path (same as FF behaviour)
+ this.beginPath();
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.stroke();
+ };
+
+ contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+ // Will destroy any existing path (same as FF behaviour)
+ this.beginPath();
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.fill();
+ };
+
+ contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+ var gradient = new CanvasGradient_("gradient");
+ return gradient;
+ };
+
+ contextPrototype.createRadialGradient = function(aX0, aY0,
+ aR0, aX1,
+ aY1, aR1) {
+ var gradient = new CanvasGradient_("gradientradial");
+ gradient.radius1_ = aR0;
+ gradient.radius2_ = aR1;
+ gradient.focus_.x = aX0;
+ gradient.focus_.y = aY0;
+ return gradient;
+ };
+
+ contextPrototype.drawImage = function (image, var_args) {
+ var dx, dy, dw, dh, sx, sy, sw, sh;
+
+ // to find the original width we overide the width and height
+ var oldRuntimeWidth = image.runtimeStyle.width;
+ var oldRuntimeHeight = image.runtimeStyle.height;
+ image.runtimeStyle.width = 'auto';
+ image.runtimeStyle.height = 'auto';
+
+ // get the original size
+ var w = image.width;
+ var h = image.height;
+
+ // and remove overides
+ image.runtimeStyle.width = oldRuntimeWidth;
+ image.runtimeStyle.height = oldRuntimeHeight;
+
+ if (arguments.length == 3) {
+ dx = arguments[1];
+ dy = arguments[2];
+ sx = sy = 0;
+ sw = dw = w;
+ sh = dh = h;
+ } else if (arguments.length == 5) {
+ dx = arguments[1];
+ dy = arguments[2];
+ dw = arguments[3];
+ dh = arguments[4];
+ sx = sy = 0;
+ sw = w;
+ sh = h;
+ } else if (arguments.length == 9) {
+ sx = arguments[1];
+ sy = arguments[2];
+ sw = arguments[3];
+ sh = arguments[4];
+ dx = arguments[5];
+ dy = arguments[6];
+ dw = arguments[7];
+ dh = arguments[8];
+ } else {
+ throw "Invalid number of arguments";
+ }
+
+ var d = this.getCoords_(dx, dy);
+
+ var w2 = sw / 2;
+ var h2 = sh / 2;
+
+ var vmlStr = [];
+
+ var W = 10;
+ var H = 10;
+
+ // For some reason that I've now forgotten, using divs didn't work
+ vmlStr.push(' ' ,
+ '',
+ '');
+
+ this.element_.insertAdjacentHTML("BeforeEnd",
+ vmlStr.join(""));
+ };
+
+ contextPrototype.stroke = function(aFill) {
+ var lineStr = [];
+ var lineOpen = false;
+ var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
+ var color = a[0];
+ var opacity = a[1] * this.globalAlpha;
+
+ var W = 10;
+ var H = 10;
+
+ lineStr.push(' max.x) {
+ max.x = c.x;
+ }
+ if (min.y == null || c.y < min.y) {
+ min.y = c.y;
+ }
+ if (max.y == null || c.y > max.y) {
+ max.y = c.y;
+ }
+ }
+ }
+ lineStr.push(' ">');
+
+ if (typeof this.fillStyle == "object") {
+ var focus = {x: "50%", y: "50%"};
+ var width = (max.x - min.x);
+ var height = (max.y - min.y);
+ var dimension = (width > height) ? width : height;
+
+ focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%";
+ focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%";
+
+ var colors = [];
+
+ // inside radius (%)
+ if (this.fillStyle.type_ == "gradientradial") {
+ var inside = (this.fillStyle.radius1_ / dimension * 100);
+
+ // percentage that outside radius exceeds inside radius
+ var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside;
+ } else {
+ var inside = 0;
+ var expansion = 100;
+ }
+
+ var insidecolor = {offset: null, color: null};
+ var outsidecolor = {offset: null, color: null};
+
+ // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie
+ // won't interpret it correctly
+ this.fillStyle.colors_.sort(function (cs1, cs2) {
+ return cs1.offset - cs2.offset;
+ });
+
+ for (var i = 0; i < this.fillStyle.colors_.length; i++) {
+ var fs = this.fillStyle.colors_[i];
+
+ colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ",");
+
+ if (fs.offset > insidecolor.offset || insidecolor.offset == null) {
+ insidecolor.offset = fs.offset;
+ insidecolor.color = fs.color;
+ }
+
+ if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) {
+ outsidecolor.offset = fs.offset;
+ outsidecolor.color = fs.color;
+ }
+ }
+ colors.pop();
+
+ lineStr.push('');
+ } else if (aFill) {
+ lineStr.push('');
+ } else {
+ lineStr.push(
+ ''
+ );
+ }
+
+ lineStr.push("");
+
+ this.element_.insertAdjacentHTML("beforeEnd", lineStr.join(""));
+
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.fill = function() {
+ this.stroke(true);
+ }
+
+ contextPrototype.closePath = function() {
+ this.currentPath_.push({type: "close"});
+ };
+
+ /**
+ * @private
+ */
+ contextPrototype.getCoords_ = function(aX, aY) {
+ return {
+ x: Z * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - Z2,
+ y: Z * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - Z2
+ }
+ };
+
+ contextPrototype.save = function() {
+ var o = {};
+ copyState(this, o);
+ this.aStack_.push(o);
+ this.mStack_.push(this.m_);
+ this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+ };
+
+ contextPrototype.restore = function() {
+ copyState(this.aStack_.pop(), this);
+ this.m_ = this.mStack_.pop();
+ };
+
+ contextPrototype.translate = function(aX, aY) {
+ var m1 = [
+ [1, 0, 0],
+ [0, 1, 0],
+ [aX, aY, 1]
+ ];
+
+ this.m_ = matrixMultiply(m1, this.m_);
+ };
+
+ contextPrototype.rotate = function(aRot) {
+ var c = mc(aRot);
+ var s = ms(aRot);
+
+ var m1 = [
+ [c, s, 0],
+ [-s, c, 0],
+ [0, 0, 1]
+ ];
+
+ this.m_ = matrixMultiply(m1, this.m_);
+ };
+
+ contextPrototype.scale = function(aX, aY) {
+ this.arcScaleX_ *= aX;
+ this.arcScaleY_ *= aY;
+ var m1 = [
+ [aX, 0, 0],
+ [0, aY, 0],
+ [0, 0, 1]
+ ];
+
+ this.m_ = matrixMultiply(m1, this.m_);
+ };
+
+ /******** STUBS ********/
+ contextPrototype.clip = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.arcTo = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.createPattern = function() {
+ return new CanvasPattern_;
+ };
+
+ // Gradient / Pattern Stubs
+ function CanvasGradient_(aType) {
+ this.type_ = aType;
+ this.radius1_ = 0;
+ this.radius2_ = 0;
+ this.colors_ = [];
+ this.focus_ = {x: 0, y: 0};
+ }
+
+ CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+ aColor = processStyle(aColor);
+ this.colors_.push({offset: 1-aOffset, color: aColor});
+ };
+
+ function CanvasPattern_() {}
+
+ // set up externs
+ G_vmlCanvasManager = G_vmlCanvasManager_;
+ CanvasRenderingContext2D = CanvasRenderingContext2D_;
+ CanvasGradient = CanvasGradient_;
+ CanvasPattern = CanvasPattern_;
+
+})();
+
+} // if
diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
new file mode 100644
index 0000000..4f4b8df
--- /dev/null
+++ b/src/main/webapp/index.html
@@ -0,0 +1,9 @@
+
+ Current Weather Conditions
+
+
+ Loading...
+
+
+
diff --git a/src/main/webapp/templates-hidden/default.html b/src/main/webapp/templates-hidden/default.html
new file mode 100644
index 0000000..1b89805
--- /dev/null
+++ b/src/main/webapp/templates-hidden/default.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ MSC Weather
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/test.html b/src/main/webapp/test.html
new file mode 100644
index 0000000..a2cccb5
--- /dev/null
+++ b/src/main/webapp/test.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ MSC Weather
+
+
+
+
+
+ Current Weather Conditions
+
+
+
diff --git a/src/test/scala/LiftConsole.scala b/src/test/scala/LiftConsole.scala
new file mode 100644
index 0000000..0d54efd
--- /dev/null
+++ b/src/test/scala/LiftConsole.scala
@@ -0,0 +1,15 @@
+import bootstrap.liftweb.Boot
+import scala.tools.nsc.MainGenericRunner
+
+object LiftConsole {
+ def main(args : Array[String]) {
+ // Instantiate your project's Boot file
+ val b = new Boot();
+ // Boot your project
+ b.boot;
+ // Now run the MainGenericRunner to get your repl
+ MainGenericRunner.main(args)
+ // After the repl exits, then exit the scala script
+ exit(0)
+ }
+}
diff --git a/src/test/scala/RunWebApp.scala b/src/test/scala/RunWebApp.scala
new file mode 100644
index 0000000..93a1d88
--- /dev/null
+++ b/src/test/scala/RunWebApp.scala
@@ -0,0 +1,28 @@
+import org.mortbay.jetty.Connector;
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.webapp.WebAppContext;
+
+object RunWebApp extends Application {
+ val server = new Server(8080);
+ val context = new WebAppContext()
+ context.setServer(server)
+ context.setContextPath("/")
+ context.setWar("src/main/webapp")
+
+ server.addHandler(context)
+
+ try {
+ println(">>> STARTING EMBEDDED JETTY SERVER, PRESS ANY KEY TO STOP");
+ server.start();
+ while (System.in.available() == 0) {
+ Thread.sleep(5000)
+ }
+ server.stop()
+ server.join()
+ } catch {
+ case exc : Exception => {
+ exc.printStackTrace()
+ System.exit(100)
+ }
+ }
+}
diff --git a/src/test/scala/uk/org/floop/msc/AppTest.scala b/src/test/scala/uk/org/floop/msc/AppTest.scala
new file mode 100644
index 0000000..735f7d5
--- /dev/null
+++ b/src/test/scala/uk/org/floop/msc/AppTest.scala
@@ -0,0 +1,29 @@
+package uk.org.floop.msc;
+
+import junit.framework._;
+import Assert._;
+
+object AppTest {
+ def suite: Test = {
+ val suite = new TestSuite(classOf[AppTest]);
+ suite
+ }
+
+ def main(args : Array[String]) {
+ junit.textui.TestRunner.run(suite);
+ }
+}
+
+/**
+ * Unit test for simple App.
+ */
+class AppTest extends TestCase("app") {
+
+ /**
+ * Rigourous Tests :-)
+ */
+ def testOK() = assertTrue(true);
+ //def testKO() = assertTrue(false);
+
+
+}