Meteorologist Exercise Guide
In this project, you will practice working with Arrays and Hashes by pulling data from external services like Google Maps. You will build an application that, given a street address, tells the user the weather forecast.
In order to achieve this, our first task will be to exchange a street address for a latitude/longitude pair using Google's Geocoding API.
We will send a location in a remarkably flexible, "English-y" format, the kind of thing we are allowed to type into a Google Maps search (e.g., "the corner of 58th and Woodlawn"), and the Geocoding API will respond with an exact latitude and longitude (along with other things) in JSON format.
Any time you are trying to develop a proof of concept of an app that needs external API data, the first step is researching the API and finding out if the information you need is available.
For us, that translates to: is there a URL that I can paste into my browser's address bar that will give back JSON (JavaScript Object Notation) that has the data I need? If so, we're good; Ruby and Rails makes the rest easy.
First, let's install a Chrome Extension called JSONView that makes working with JSON easier:
This will indent the JSON nicely and allow you to fold and unfold nested elements.
We will provide you with an API key Google Maps. You'll be able to find it by visiting the assignment page for Omnicalc-Acions on Canvas. You'll need this key to complete the homework exercises below.
We'll also need to make sure your API key stays hidden, in case your project ever gets pushed to GitHub or another public repository. Unsavory types like to scrape GitHub for sensitive information like API keys and run up huge bills for compromised users.
We can do this fairly easily in your workspace:
If you realize you've made an error on any of the following steps, just type incd ~/workspace
and hit enter. That should get you back to the starting point.
- 1.Type in
cd ~
and hit enter. This command should take you to the home folder of your workspace. - 2.Type in
touch .bash_profile
and hit enter. This command creates a hidden file called.bash_profile
in your home folder. - 3.Type in
ls -a
and hit enter. You'll see a list of all the files in your current directory, including hidden files (the ones whose names start with a.
). - 4.Mouse over the filename
.bash_profile
and click it. The file should open up in your editor. - 5.Paste in the following code into the file but make sure to use the Google API key on the right side of the
=
signexport GOOGLE_MAPS_KEY="replace_me_with_your_key"Note: don't put spaces around the=
. - 6.Back in Terminal, type in
cd ~/workspace
to go back to your main folder, or just close that tab.If you ever need to reopen your bash profile, typecd ~
, hit enter, then typels -a
, hit enter and click on the file to open it. - 7.If you have a Rails server running, stop it and re-start it.
- 8.The values that we
export
in the.bash_profile
file become key-value pairs in a special Hash that we can access anywhere in our Rails environment calledENV
. For example, to access this sensitive info, in a NEW Terminal tab (it won't work in old tabs) we can open arails console
and type in:ENV.fetch("GOOGLE_MAPS_KEY")and we should see output of=> "replace_me_with_your_key"
You can use this pattern throughout your Rails app to store and use sensitive info but prevent it from showing on GitHub when you push your code up, or from unauthorized visitors being able to see it in your public Cloud9 workspaces.
Now, we have to research the API and find the correct URL to paste into our address bar to get back the JSON that we want. Usually, we have to start at the API Documentation. For the Geocoding API, the full docs are here:
but, as usual with technical documentation, it's best once you skim the intro to head straight for the examples:
The first example they give is
(I have removed the part about the API key from the end of the URL. You can find it on the Canvas assignment page.) Replace
YOUR_API_KEY
with the one provided to you. Paste that URL into a Chrome tab; you should see something like this:
Try folding away the
address_components
section to make the value of the geometry
key stand out, since that is where our target information lives:
Notice the
lat
and lng
keys within the location
hash. Notice that JSON uses curly braces for Hashes and square brackets for Arrays just like Ruby does.Alright, now that we have the data we need showing up in the browser window, what's next? First of all, we should be sure that we can customize this example request to get data that we care about. So how would we get the coordinates of "5807 S Woodlawn Ave" instead of Google's HQ? Give it a try.
No really, try it yourself!
You'll have to modify the example URL somehow.
It turns out we need to replace the part of the URL after the
?address=
with the address we want geocoded:
(Spaces are not legal in URLs, so we have to encode them. One way to encode them can be seen in Google's example, with
+
s. If you tried typing the address with spaces in it, you'll have noticed that Google encodes spaces automatically with %20%
.)Great! Now we know the exact data we want is available through the API. Now, how do we get it into our application? Fortunately, Ruby comes with some powerful built-in functionality that will help us with this. First, we need Ruby to read the URL that has the JSON we want, just like Chrome did. Chrome has an address bar we can paste the URL in to; how can we tell Ruby to go open a page on the Internet?
If you haven't already,
- create a workspace,
bin/setup
,- and then in a Terminal tab, enter the command
rails console
. This launches an interactive Ruby sandbox for us to experiment in.
In the Rails Console, let's use Ruby's
open()
method to read Google's page. The open()
method takes one String
argument, which should contain the URL of the page you want to open. I'm going to copy-paste the URL within " "
and store it in a variable url
to make it easier to work with:url = "https://maps.googleapis.com/maps/api/geocode/json?address=5807+S+Woodlawn+Ave&key="
+ ENV.fecth("GOOGLE_MAPS_KEY")
Then, let's
open
that URL and read
the body of the page:open(url).read
You should see something like this:

Note: To scroll through long output inrails console
, you can use return to scroll one line at a time, Space to scroll one page at a time, or Q to just get back to the prompt to enter a new Ruby expression.
What just happened? We
open
ed the page at the location in url
, and the return value was the HTTP response. The HTTP response is actually a complicated object, with headers and status codes and other things we haven't talked about yet.All we really want is the body of the response, the stuff that shows up in the browser window, so we used the
.read
method to pull that out. However, we just dropped that string on the ground; let's instead store the result in a variable called raw_data
:raw_data = open(url).read
Alright! We just used Ruby to open up a connection over the Internet to Google's servers, placed a request for them to translate our address into a latitude and longitude, received a response, and stored it in a Ruby variable! That's a big deal, folks. However, the response is hideous. How in the world are we going to pull out the latitude and longitude values from that thing?
We could explore the String class documentation and find some methods that might help us scan through
raw_data
for "lat"
, perhaps. But then what? We could probably figure it out, but there's a much better way.Fortunately, Ruby provides a class called
JSON
, similar to the CSV
class, which makes parsing a string that has data in JSON format a snap:parsed_data = JSON.parse(raw_data)
and you should see
=> {"results"=>
[{"address_components"=>
[{"long_name"=>"Chicago Booth Harper Center",
"short_name"=>"Chicago Booth Harper Center",
"types"=>["premise"]},
{"long_name"=>"5807", "short_name"=>"5807", "types"=>["street_number"]},
{"long_name"=>"South Woodlawn Avenue", "short_name"=>"S Woodlawn Ave", "types"=>["route"]},
{"long_name"=>"South Side", "short_name"=>"South Side", "types"=>["neighborhood", "political"]},
{"long_name"=>"Chicago", "short_name"=>"Chicago", "types"=>["locality", "political"]},
{"long_name"=>"Cook County",
"short_name"=>"Cook County",
"types"=>["administrative_area_level_2", "political"]},
{"long_name"=>"Illinois", "short_name"=>"IL", "types"=>["administrative_area_level_1", "political"]},
{"long_name"=>"United States", "short_name"=>"US", "types"=>["country", "political"]},
{"long_name"=>"60637", "short_name"=>"60637", "types"=>["postal_code"]},
{"long_name"=>"1610", "short_name"=>"1610", "types"=>["postal_code_suffix"]}],
"formatted_address"=>"Chicago Booth Harper Center, 5807 S Woodlawn Ave, Chicago, IL 60637, USA",
"geometry"=>
{"bounds"=>
{"northeast"=>{"lat"=>41.789511, "lng"=>-87.5949885},
"southwest"=>{"lat"=>41.7883316, "lng"=>-87.5961513}},
"location"=>{"lat"=>41.7891369, "lng"=>-87.5954551},
"location_type"=>"ROOFTOP",
"viewport"=>
{"northeast"=>{"lat"=>41.79027028029149, "lng"=>-87.59422091970849},
"southwest"=>{"lat"=>41.7875723197085, "lng"=>-87.59691888029151}}},
"place_id"=>"ChIJkb9VkxYpDogRSmVtpZC9s6s",
"types"=>["premise"]}],
"status"=>"OK"}
Look! Hash rockets! We've converted a cumbersome string into a beautiful, friendly Ruby Hash. Now, if I want to get down to the latitude and longitude, how would I do it? Well, remember, we already got a hint back when we first looked at the data in Chrome: it has something to do with the
geometry
key. We can remind ourselves what keys are in the hash:parsed_data.keys
=> ["results", "status"]
The data we want is probably inside the key "results". So lets step in one level deep and see what we have:
parsed_data.fetch("results").class
=> Array
Let's go one level deeper by getting the first element and seeing what it is:
Note: As you explore, don't forget that you can use your UP ARROW to scroll through your command line history. You don't always have to re-type the same thing over and over!
f = parsed_data.fetch("results").at(0)
f.class
=> Hash
Another hash — let's see what keys it has:
f.keys
=> ["address_components", "formatted_address", "geometry", "place_id", "types"]
Alright, so we now have a Hash in
f
that has a key called "geometry"
, which, as we learned from our initial research above, is what contains our target. Let's keep going:f.fetch("geometry")
=> {"bounds"=>
{"northeast"=>{"lat"=>41.789511, "lng"=>-87.5949885},
"southwest"=>{"lat"=>41.7883316, "lng"=>-87.5961513}},
"location"=>{"lat"=>41.7891369, "lng"=>-87.5954551},
"location_type"=>"ROOFTOP",
"viewport"=>
{"northeast"=>{"lat"=>41.79027028029149, "lng"=>-87.59422091970849},
"southwest"=>{"lat"=>41.7875723197085, "lng"=>-87.59691888029151}}}
Closer!
f.fetch("geometry").fetch("location")
=> {"lat"=>41.7891369, "lng"=>-87.5954551}
Almost there!
f.fetch("geometry").fetch("location").fetch("lat")
=> 41.7891369
f.fetch("geometry").fetch("location").fetch("lng")
=> -87.5954551
Woo! We made it all the way down to what we want. Phew! Now, I did it in a bunch of tiny steps, which might have made it seem complicated, but we could also have just done it in one step:
parsed_data.fetch("results").at(0).fetch("geometry").fetch("location").fetch("lng")
=> -87.5954551
I prefer working in small steps and peeling one layer off at a time while I am exploring. But, once I know what I need, there's also a method called
dig
that can help us drill down into nested Hash/Array structures like this a bit more concisely:parsed_data.dig("results", 0, "geometry", "location", "lng")
=> -87.5954551
So, the entire program to geocode boils down to just four lines!
url = "https://maps.googleapis.com/maps/api/geocode/json?address=5807+S+Woodlawn+Ave&key=" + ENV.fetch("GOOGLE_MAPS_KEY")
parsed_data = JSON.parse(open(url).read)
latitude = parsed_data.dig("results", 0, "geometry", "location", "lat")
longitude = parsed_data.dig("results", 0, "geometry", "location", "lng")
And now I can do whatever interesting things with
latitude
and longitude
that I need.Now that we've explored in the console, it's time to take these four lines (or something similar to them, anyway) and write some permanent programs...
Now that we've seen how to retrieve information from a URL using Ruby, let's plug it in to a real application. If you haven't already, launch your Rails app with
bin/server
and navigate to the homepage in a Chrome tab.I have started you off with three forms:
- Street → Coords
- Coords → Weather
- Street → Weather
And three files which process these form inputs and render results:
app/controllers/geocoding_controller.rb
app/controllers/forecast_controller.rb
app/controllers/meteorologist_controller.rb
We'll be working on Street → Coords first.
Open the file
app/controllers/geocoding_controller.rb
. Your job is to write some code in the street_to_coords
method, where indicated, and put the correct value in the @latitude
and @longitude
variables.If I type in
5807 S Woodlawn Ave
at the Street → Coords form, I should see something likeStreet Address5807 S Woodlawn AveLatitude41.7896234Longitude-87.5964137
Next, in
app/controllers/forecast_controller.rb
, you will do something similar; but instead of using Google's Geocoding API, you will use The Forecast API. We will exchange a latitude/longitude pair for weather information. Forecast is an amazing API that gives you minute-by-minute meteorological information.They released an iOS app, Dark Sky, to demonstrate the power of their API, and it instantly became a smash hit. The API is not entirely free, but we get 1,000 calls per day to play around with.
Step 1 when working with any API is research. What is the URL of the page that has the data we want, preferably in JSON format?




Once you've registered, verified your email, and signed in, you will be given a sample API call with your own personal API key inserted in it:

Click the example link and check out the data they provide. Scroll up and down and get a feel for it:

It's pretty amazingly detailed data; it tells us current conditions, along with minute-by-minute conditions for the next hour, hour-by-hour conditions for the next day or so, etc.
But first, can we customize the example to get data relevant to us? Plug in some coordinates that you fetched in
street_to_coords
and try it out.Your job is to write some code in the
coords_to_weather
method, where indicated, and put the correct value in the instance variables at the end.If I type in
41.78
and -87.59
at the Coords → Weather form, I should see something likeLatitude41.78Longitude-87.59Current Temperature73.35Current SummaryClearOutlook for next sixty minutesClear for the hour.Outlook for next several hoursPartly cloudy tomorrow morning.Outlook for next several daysNo precipitation throughout the week, with temperatures falling to 62°F on Tuesday.
Note: Forecast does not have data for every lat/lng combination; some geographies will return
nil
s. If you run into issues, using dig
might be helpful, since it doesn't throw an error if a key is missing along the way:parsed_results.dig("minutely", "summary")
rather than
parsed_results.fetch("minutely").fetch("summary")
Finally, pull it all together in
app/controllers/meteorologist_controller.rb
. Use both the Google Geocoding API and the Forecast API so that if I type in 5807 S Woodlawn Ave
at the Street → Weather form, I should see something likeHere's the outlook for 5807 S Woodlawn Ave:Current Temperature73.35Current SummaryClearOutlook for next sixty minutesClear for the hour.Outlook for next several hoursPartly cloudy tomorrow morning.Outlook for next several daysNo precipitation throughout the week, with temperatures falling to 62°F on Tuesday.
Run
rails grade:all
at a Terminal prompt when you're ready for feedback and your score. You can run it as many times as you want.Browse
and get inspired!
<link>
to Bootstrap or a Bootswatch in the <head>
of your pages (located in app/views/layouts/application.html.erb
), and make things look prettier.The Forecast API can take a third parameter in the URL, time:
Add a feature that shows summaries for the next 14 days.
Embed a Google map in the view, centered on the provided address. Refer to the docs:
The key concept is, just like with Bootstrap, to first paste in the example markup and see if it works.
Then, replace whichever part of the static markup you want to with embedded Ruby tags that contain your dynamic values.
Last modified 4yr ago