James Williams
LinkedInMastodonGithub

Griffon Weather App

Tags: Griffon

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)