Elm, Geocoding & DarkSky: Pt. 3 – Fetching the Current Weather
Info
Summary | In Part 3 we query the DarkSky API with our geocoded address |
---|---|
Shared | 2017-08-18 |
This is part 3 of a multipart series where we will be building a small weather forecast app using Elm, Google’s Geocoding API and the DarkSky API. Instead of doing everything in one massive post, I’ve broken the steps down into parts of a series. Here is the series plan:
- Pt. 1 – Setup Elm & Proxy Servers
- Pt. 2 – Geocoding an Address
- Pt. 3 – Fetching the Current Weather
- Pt. 4 – Extracting Our Elm Code
If you’d like to code along with this tutorial, check out part 1 and part 2 first to get set up.
Note: to learn more about the Elm language and syntax, check out the Elm Tutorial, the EggHead.io Elm course, subscribe to DailyDrip’s Elm Topic, James Moore’s Elm Courses or check out Elm on exercism.io.
Overview
In this post we will use Elm to fetch and display the current weather based on the geocode data we receive from an input field.
Project Source Code
The project we’re making will be broken into parts here (branches will be named for each part): https://github.com/rpearce/elm-geocoding-darksky/. Be sure to check out the other branches to see the other parts as they become available.
The code for this part is located in the pt-3
branch: https://github.com/rpearce/elm-geocoding-darksky/tree/pt-3.
Steps for Today
- Understanding DarkSky’s response data
- Modeling the DarkSky response data
- Creating DarkSky JSON decoders
- Writing our fetchWeather HTTP function
- Calling fetchWeather and handling the response
- Displaying the current weather in our view
1. Understanding DarkSky’s response data
Let’s get the weather data for Auckland, NZ (-36.8484597,174.7633315). If we start up our DarkSky proxy and run
λ curl localhost:5051/forecast/-36.8484597,174.7633315
then we will see response data like this:
{
"timezone": "Pacific\/Auckland",
"currently": {
"summary": "Overcast",
"icon": "cloudy",
"temperature": 61.42,
...
},
"hourly": { ... },
"daily": { ... }
}
While all we care about are the summary
, icon
and temperature
properties within the top-level currently
property, we will only use temperature
in this part.
Disclaimer: DarkSky units are in us
by default. You can specify other unit types by appending a units
query parameter to the end like this:
λ curl localhost:5051/forecast/-36.8484597,174.7633315?units=si
Read more about DarkSky request parameters in the DarkSky docs to customize your response data.
Now that we’ve got our data in the correct units, let’s model this data in Elm!
2. Modeling the DarkSky response data
Based on our DarkSky response, let’s list out what we’re looking at:
- an object,
currently
, which has 3 notable properties:- a string,
summary
- a string,
icon
- a float,
temperature
- a string,
Since we have two levels of data, currently
and its child properties, let’s create two type aliases to represent this data.
type alias Weather =
{ currently : WeatherCurrently
}
type alias WeatherCurrently =
{ icon : String
, summary : String
, temperature : Float
}
And now we can add a property to our Model
type alias that can be of our Weather
type:
type alias Model =
{ address : String
, coords : Coords
, weather : Weather
}
Uh oh! Our Model
has a defaults function called initialModel
, and now that we’ve added weather
into the mix, we’ll need to give that default values, as well:
initialModel : Model
initialModel =
{ address = ""
, coords = ( 0, 0 )
, weather = initialWeather
}
initialWeather : Weather
initialWeather =
{ currently = initialWeatherCurrently
}
initialWeatherCurrently : WeatherCurrently
initialWeatherCurrently =
{ icon = "–"
, summary = "–"
, temperature = 0
}
These are defaults that we provide in the event that we have no data to work with (initially or if something goes wrong).
3. Creating DarkSky JSON decoders
Just as we did in the geocoding post section on JSON decoding, we want to leverage NoRedInk’s elm-decode-pipeline to define how our JSON response should be structured and thus parsed.
decodeWeather : Decoder Weather
decodeWeather =
decode Weather
|> required "currently" decodeWeatherCurrently
decodeWeatherCurrently : Decoder WeatherCurrently
decodeWeatherCurrently =
decode WeatherCurrently
|> required "icon" string
|> required "summary" string
|> required "temperature" float
While we could use Json.Decode.at to potentially have less code, there is absolutely nothing wrong with being verbose if it leads to clarity.
4. Writing our fetchWeather HTTP function
We know that we’re going to have to send latitude and longitude Coords
to our DarkSky proxy server, as well as any additional options, so let’s define the URL for that and the fetching function just like we did for geocoding.
weatherUrl : Coords -> String
weatherUrl ( lat, lng ) =
"http://localhost:5051/forecast/"
++ (toString lat)
++ ","
++ (toString lng)
-- this is where you can add your query params
fetchWeather : Coords -> Cmd Msg
fetchWeather coords =
Http.get (weatherUrl coords) decodeWeather
|> Http.send ReceiveWeather
To define an HTTP request in Elm, we need * ✅ a URL to point to * ✅ a package like Http to help us build the request * ✅ a decoder to handle parsing the response data * 🤷 a Msg
type that our update
function can pattern match on
Right! We can’t forget to add ReceiveWeather
as a Msg
type. It should be almost the same as ReceiveGeocoding
:
type Msg
= UpdateAddress String
| SendAddress
| ReceiveGeocoding (Result Http.Error GeoModel)
| ReceiveWeather (Result Http.Error Weather)
| NoOp
5. Calling fetchWeather and handling the response
When we handled our geocode response in the prior post, inside of ReceiveGeocoding
we returned ( newModel, Cmd.none )
, for we had no further actions to take. Instead of our action in this tuple being Cmd.none
, let’s instead call our fetchWeather
function and pass it our geocoded coordinates:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- ...removed for brevity
ReceiveGeocoding (Ok { results, status }) ->
let
-- ...
newModel =
-- ...
in
( newModel, fetchWeather newModel.coords )
-- ...
ReceiveWeather (Ok resp) ->
( { model | weather = { currently = resp.currently } }
, Cmd.none
)
ReceiveWeather (Err _) ->
( model, Cmd.none )
Again, at the end of ReceiveGeocoding
, we return our newModel
as well as the command to go and fetch the weather with the coordinates we’re storing on our newModel
.
Whenever the HTTP request and decoding gives us back a result with the Msg
type of ReceiveWeather
, we then update the weather property on our model record to have the currently
data parsed from the decoder.
6. Displaying the current weather in our view
Finally, to make sure we’re doing each step correctly, let’s add the temperature to our view:
view : Model -> Html Msg
view model =
div []
[ form [ onSubmit SendAddress ]
[ input
[ type_ "text"
, placeholder "City"
, value model.address
, onInput UpdateAddress
]
[]
]
, p [] [ text ("Coords: " ++ (toString model.coords)) ]
, p [] [ text ("Weather: " ++ (toString (round model.weather.currently.temperature))) ]
]
Here we use Basics.round because an approximation is alright for weather.
Now, when you rebuild your code with ./build
, open index.html
and submit a city/address name, you’ll first see the Coords
update on the page and then see the Weather
result once it’s done.
Wrapping Up
Hooray! We can geocode an address and fetch the weather via two different proxy servers and display a result! That’s great, but our Main.elm
file is getting quite large, so stay tuned for the next part where we pull our code into smaller chunks without losing clarity.
If you’d like to check out the code from this part, it is located here: https://github.com/rpearce/elm-geocoding-darksky/tree/pt-3.
Until next time,
Robert