Prerequisites:
- Ruby
- sqlite3
- Rails Installer
Install the app
rails new air_quality_app
serve the application
cd air_quality_app
bin/rails server
visit your browser with this link http://127.0.0.1:3000, you should have a similar display as below
Create a controller
To create FeedsController and its index action, run the controller generator in the terminal.
bin/rails generate controller Feeds index
install the Waqi Ruby SDK
Add this line to your application's Gemfile:
gem 'waqi-ruby'
And then execute:
bundle
Or install it yourself as:
gem install waqi-ruby
You need a token to conveniently access the Air QUality Open Data Platform API. Visit https://aqicn.org/data-platform/token/ to get a token.
Open your terminal to save the token
EDITOR=vim rails credentials:edit
You can change the editor to your preferred choice like nano.
Add the following line and save the file.
air_quality_data_token: <insert-the-api-token-here>
You should be able to access the token using the code: Rails.application.credentials[:air_quality_data_token]
Add some styling to the view
Use tailwind css for the styling. A tailwind ruby gem is available at https://github.com/rails/tailwindcss-rails, you can check it out for more details.
install tailwindcss
bundle add tailwindcss-rails
rails tailwindcss:install
The above commands will create files and folders necessary to kickstart designing with tailwind: app/views/layouts/application.html.erb
, app/assets/builds/
, config/tailwind.config.js
, app/assets/stylesheets/application.tailwind.css
, Procfile.dev
, bin/dev
.
Additionally, you can install foreman gem to run Tailwind in "watch" mode concurrently. Style changes are automatically reflected in the generated CSS output.
To configure tailwind to be applied in all template files, you can edit config/tailwind.config.js
file.
Now serve the application: bin/dev
This will run both the rail server and tailwind watch.
Adjust your application layout to use nice UI. Update the body section of app/views/layouts/application.html.erb
file with the code below:
...
<body class="flex flex-col justify-between">
<header class="bg-gray-500 text-white shadow-lg">
<nav class="mx-auto flex max-w-7xl items-center justify-between gap-x-6 p-6 lg:px-8" aria-label="Global">
<h1 class="font-bold md:text-2xl m-0">
WAQI Essentials
</h1>
<div class="flex flex-1 items-center justify-end gap-x-6">
<p class="text-white text-sm font-semibold leading-6">City Feed</p>
<p class="text-white text-sm font-semibold leading-6">Search</p>
</div>
</nav>
</header>
<main>
<div class="max-w-7xl mx-auto p-6 lg:px-8 prose">
<%= yield %>
</div>
</main>
</body>
...
Edit app/views/feeds/index.html.erb
to use the parent layout that has the tailwind file configured.
<div class="text-center">
<h1 class="text-3xl font-bold">City Feed</h1>
<p>Get real-time Air Quality index for a given station.</p>
</div>
<form>
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-gray-300">Search</label>
<div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
<input type="search" id="default-search" class="block p-4 pl-10 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="enter name of city..." required>
<button type="submit" class="text-white absolute right-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Search</button>
</div>
</form>
You should have a result as below:
Let's interact with api by submitting a city name and get a result.
First, modify the form tag to conform to rails code standard!
...
<%= form_with url: "/feeds/index", method: :get do |form| %>
<%= form.label :city, "Search", class:"mb-2 text-sm font-medium text-gray-900 sr-only dark:text-gray-300" %>
<div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
<%= form.text_field :city, placeholder: "enter name of city...", autofocus: true, required: true, value: @city,
class:"block p-4 pl-10 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
%>
<%= form.submit "Search", class: "text-white absolute right-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" %>
</div>
<% end %>
<div class="text-center">
<p><%= @feeds %></p>
</div>
You should notice the form_url tag has the url '/feeds/index'. After submitting the form the request goes to the index action on feeds_controller.
Modify the controller to accept the request and connect to Air Quality API to get a result:
# app/controllers/feeds_controller.rb
require 'waqi'
class FeedsController < ApplicationController
before_action :set_client
def index
@city = request.query_parameters['city']
if !@city.blank?
@feeds = @waqi_client.get_city_feed(@city)
end
end
private
def set_client
@waqi_client = Waqi::Client::new(api_key: Rails.application.credentials[:air_quality_data_token])
end
end
The above code does the following:
- Loads the https://github.com/waqi-dev-community/waqi-ruby-client at the top of the file.
- A before_action ruby method initialize set_client method. This method allows us to use the library in the code.
- Index method accepts the query parameter
city
to be used.
When you search for a city let's say new-york
, you should get this result in your browser:
You can beautify your result and integrate a Google line chart to display daily forecast for Ozone and Particulate matter level:
Add a script tag to app/views/layouts/application.html.erb
file
...
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load('current', {'packages':['line']});
var forecast = document.getElementById('line_top_x').getAttribute('data-forecast')
if(forecast.length) {
google.charts.setOnLoadCallback(drawChart);
}
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Date');
data.addColumn('number', 'Ozone (o3)');
data.addColumn('number', 'Particulate Matter 10 (pm10)');
data.addColumn('number', 'Particulate Matter 25 (pm25)');
data.addRows(JSON.parse(forecast));
var options = {
width: 900,
height: 500,
axes: {
x: {
0: {side: 'top'}
}
}
};
var chart = new google.charts.Line(document.getElementById('line_top_x'));
chart.draw(data, google.charts.Line.convertOptions(options));
}
</script>
</body>
</html>
The script loads the chart library and the forecast data is inserted by getting the data attribute on the page app/views/feeds/index.html.erb
:
var forecast = document.getElementById('line_top_x').getAttribute('data-forecast')
Insert this code snippet below into the city_feed views page:
<% if @isError %>
<div class="m-4 p-2 text-red-500">
<span>Error: <%= @response["data"] %></span>
</div>
<% else %>
<div class="text-center grid grid-flow-row-dense grid-cols-4 grid-rows-4">
<p><span class="font-black">City: </span> <%= @city_info["name"] %></p>
<p><span class="font-black">Latitude: </span><%= @city_info["geo"][0] %></p>
<p><span class="font-black">Longitude: </span><%= @city_info["geo"][1] %></p>
<% if !@iaqi.blank? %>
<% @iaqi.each do |iaqi| %>
<% extra = determineLabel(iaqi["name"]).to_h %>
<p><span class="font-black"><%= extra["label"] %>: </span><%= iaqi["value"] %> <%= extra["unit"] %></p>
<% end %>
<% end %>
</div>
<div class="text-center">
<h3> Daily forecast for Ozone, Particulate Matter 10 and 25</h3>
<div id="line_top_x" style="margin-right: auto;margin-left: auto;display: table;" data-forecast="<%= @daily_forecast %>"></div>
</div>
<% end %>
The code above handles the error if there's any and also display the city, latitude, longitude, and other air quality information for a particular station.
This line of code <% extra = determineLabel(iaqi["name"]).to_h %>
is used to format the data with readable texts and provide more information to the user. You can update the app/helpers/application_helper.rb
with the code below. This is a helper class.
module ApplicationHelper
def determineLabel(name)
case name
when "co"
{ "label" => "Carbon monoxide", "unit" => "ppm" }
when "no2"
{ "label" => "Nitrogen dioxide", "unit" => "ppb" }
when "pm10"
{ "label" => "PM10", "unit" => "µg/m³" }
when "pm25"
{ "label" => "PM25", "unit" => "µg/m³" }
when "o3"
{ "label" => "Ozone", "unit" => "ppb" }
when "w"
{ "label" => "Wind Speed", "unit" => "m/s" }
when "t"
{ "label" => "Temperature", "unit" => "°C" }
when "p"
{ "label" => "Air pressure", "unit" => "mb" }
when "so2"
{ "label" => "Sulfur dioxide", "unit" => "ppb" }
when "h"
{ "label" => "Humidity", "unit" => "%"}
when "dew"
{ "label" => "Dew", "unit" => "°C"}
end
end
end
Update the index method on FeedsController file:
def index
@city = request.query_parameters['city']
if !@city.blank?
response = @waqi_client.get_city_feed(@city)
if response["status"] == "ok"
@response = response["data"]
@city_info = response["data"]["city"]
@iaqi = response["data"]["iaqi"].map do |key, value|
{ "name" => key.to_s, "value" => value["v"] }
end
@forecast = response["data"]["forecast"]["daily"]
@daily_forecast = format_forecast_data(@forecast)
elsif response["status"] == "error"
@isError = true
@response = response
end
end
end
private
...
def format_forecast_data(air_quality_data)
# Extract days from "o3", "pm10", and "pm25" arrays
o3_days = air_quality_data["o3"].map { |data| data["day"] } || []
pm10_days = air_quality_data["pm10"].map { |data| data["day"] } || []
pm25_days = air_quality_data["pm25"].map { |data| data["day"] } || []
# Find intersection of the three arrays
identical_days = o3_days & pm10_days & pm25_days
formatted_parameter_data = []
# Extract required values and push into new array
identical_days.each do |date|
o3_index = air_quality_data["o3"].find_index { |entry| entry["day"] == date }
pm10_index = air_quality_data["pm10"].find_index { |entry| entry["day"] == date }
pm25_index = air_quality_data["pm25"].find_index { |entry| entry["day"] == date }
formatted_parameter_data << [date, air_quality_data["o3"][o3_index]["avg"], air_quality_data["pm10"][pm10_index]["avg"], air_quality_data["pm25"][pm25_index]["avg"]]
end
formatted_parameter_data
end
You can explore more functionality with the ruby sdk such as Lat/Lng based Geolocalized feed, IP based Geolocalized feed, etc.
The complete source code can be found on github
Top comments (0)