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} + barometer + AVERAGE + + + barometerMb + barometer,33.86,* + + + + + barometerMb + #FF0000 + Barometric pressure + + + \ 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} + rainRate + AVERAGE + + + + + rainRate + #FF0000 + Rain rate + + + \ 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} + inTemp + AVERAGE + + + inTempC + inTemp,32,-,9,/,5,* + + + outTemp + ${rrdFile} + outTemp + AVERAGE + + + outTempC + outTemp,32,-,9,/,5,* + + + + + inTempC + #FF0000 + Inside Temperature + + + outTempC + #0000FF + Outside Temperature + + + \ 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} + inTemp + MIN + + + inTempMinC + inTempMin,32,-,9,/,5,* + + + outTempMin + ${rrdFile} + outTemp + MIN + + + outTempMinC + outTempMin,32,-,9,/,5,* + + + + + inTempMinC + #FF0000 + Inside Minimum Temperature + + + outTempMinC + #0000FF + Outside Minimum Temperature + + + \ 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} + windSpeed + AVERAGE + + + windSpeedKnots + windSpeed,0.868976242,* + + + windGust + ${rrdFile} + windGust + MAX + + + windGustKnots + windGust,0.868976242,* + + + + + windSpeedKnots + #FF0000 + Wind speed + + + windGustKnots + #0000FF + Gusting + + + \ 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} + windDir + AVERAGE + + + + + windDir + #FF0000 + Wind direction + + + \ 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); + + +}