Griffon Weather App
James Clarke recently updated his JSON Weather Service application to fête the release of JavaFX 1.0. I happened upon it on DZone and decided to port it to Griffon. The original source is 227 lines of code whereas the Griffon version is 151.
Here's the code breakdown:
Name | Files | LOC |
---|---|---|
Controllers | 1 | 38 |
Models | 1 | 13 |
Views | 1 | 28 |
Lifecycle | 5 | 68 |
Integration Tests | 1 | 4 |
Total | 9 | 151 |
The first major difference is that instead of an instance class Weather, all the variables live in the GriffonWeatherServiceModel.groovy.
import groovy.beans.Bindable
class GriffonWeatherServiceModel {
@Bindable stationName = ''
@Bindable clouds = ''
@Bindable windDirection = 0
@Bindable windSpeed = 0
@Bindable temperature = 0
@Bindable dewPoint = 0
@Bindable humidity = 0
@Bindable seaLevelPressure = 0
@Bindable observation = ''
@Bindable code = ''
}
One place where I was able to condense code was in GriffonWeatherServiceController.groovy.
import net.sf.json.JSONArray
import javax.swing.JOptionPane
import net.sf.json.groovy.JsonSlurper
class GriffonWeatherServiceController {
// these will be injected by Griffon
def model
def view
void mvcGroupInit(Map args) {
// this method is called after model and view are injected
}
def parseJSON = { evt = null, text ->
JsonSlurper slurper = new JsonSlurper()
def json = slurper.parseText(text)?.get('weatherObservation')
if (json != null) {
for (prop in model.getProperties()) {
if (json.get(prop.key) != null) {
model.{prop.key} = json.getString(prop.key)
}
}
} else {
JOptionPane.showMessageDialog(null, "Either the code is invalid or there was a problem.", "error", JOptionPane.ERROR_MESSAGE)
}
}
def getJSON = { url ->
doLater {
app.appFrames[0].getGlassPane().setSize(app.appFrames[0].getSize())
app.appFrames[0].getGlassPane().setVisible(true)
app.appFrames[0].getGlassPane().setBusy(true)
}
doOutside{
def result = new java.net.URL(url).getText()
parseJSON(null, result)
app.appFrames[0].pack()
app.appFrames[0].getGlassPane().setBusy(false)
app.appFrames[0].getGlassPane().setVisible(false)
}
}
}
Instead of walking the tree for each attribute, I inspect the properties of the model and see if they exist in the JSONObject. If so, they are assigned. If an incorrect code is entered (or there is no Internet), an error dialog appears. Retrieving and parsing the JSON is done outside the EDT.
Another area of major savings was in the view(GriffonWeatherServiceView.groovy). I was able to declare the functionality of a label/value pair and repeatedly invoke the defined closures.
import net.miginfocom.swing.MigLayout
application(title:'Griffon Weather', pack:true, size:[700,400], pack:true, locationByPlatform:true, layout:new MigLayout()) {
label("Airport Code:")
textField(id:'tf',columns:5, constraints:'spanx',
actionPerformed:{controller.getJSON('http://ws.geonames.org/weatherIcaoJSON?ICAO=K'+tf.text?.toUpperCase())
})
def createHorizontalBox = { fieldName, boundParam ->
label(fieldName, constraints:"newline")
label(id:fieldName.replace(' ','_')+'Label', foreground: java.awt.Color.BLUE,
text:bind(source:model, sourceProperty:boundParam), constraints:'wrap'
)
}
def createHorizontalBoxWithSuffix = { fieldName, boundParam, suffix ->
label(fieldName, constraints:'newline')
label(id:fieldName.replace(' ','_')+'Label', foreground: java.awt.Color.BLUE,
text:bind(source:model, sourceProperty:boundParam, converter:{it + suffix}), constraints:"wrap, spanx"
)
}
createHorizontalBox('Station: ', "stationName")
createHorizontalBox('Clouds: ', "clouds")
createHorizontalBoxWithSuffix('Wind Direction: ', "windDirection", " degrees")
createHorizontalBoxWithSuffix('Wind Speed: ', "windSpeed", " knots")
createHorizontalBoxWithSuffix('Temperature: ', "temperature", " C degrees")
createHorizontalBoxWithSuffix('Dew Point: ', "dewPoint", " C degrees")
createHorizontalBoxWithSuffix("Humidity: ", "humidity", "%")
createHorizontalBoxWithSuffix("Sea Level Pressure: ", "seaLevelPressure", "mb")
// createHorizontalBox("METAR Observation: ", "observation")
}
On startup, I declared a JXBusyLabel to live on our GlassPane and indicate when the system is busy. From Startup.groovy:
import java.awt.Dimension
import java.awt.geom.*
import org.jdesktop.swingx.JXBusyLabel
import org.jdesktop.swingx.painter.BusyPainter
def busyLabel = new JXBusyLabel(app.appFrames[0].getSize())
busyLabel.setOpaque(false)
busyLabel.setBusy(false)
busyLabel.getBusyPainter().setHighlightColor(java.awt.Color.DARK_GRAY)
busyLabel.getBusyPainter().setPointShape(new Ellipse2D.Float(1,1,10,10))
app.appFrames[0].setGlassPane(busyLabel)