Contents


Build a weather-tracking application in the cloud, Part 1

It's easy with PHP, jQuery Mobile, and IBM Insights for Weather

Comments

Remember tuning in to the evening news for the next day's weather forecast? Those days are long gone! Today, weather information is updated in real time and available online, through any number of weather-focused applications and APIs. So, if you're a developer looking to build weather information into your mobile or web application, you're at the right place and time.

How do you get started? That's where this tutorial comes in. In this Part 1, I show how to create the base application and retrieve and store latitude/longitude coordinates for world cities from an online service. Then, in Part 2, I'll introduce you to the new Insights for Weather service from IBM and show you how to use the city information from Part 1 to track the weather in five world cities of your choice, and then host the final application on IBM Bluemix.

Run the example appGet the code for the example app

What you'll need for your app

The example application that is described in this tutorial allows users to select up to five cities for weather-tracking purposes. The application retrieves current temperature and weather conditions for each city and displays this information in a mobile-optimized view, suitable for use in both smartphones and desktop computers. The user can also access a seven-day forecast for each city, with all weather information sourced via Insights for Weather.

On the client side of things, I'll be using jQuery Mobile to create a mobile-friendly user interface for the application. On the server, I'll use the Silex PHP micro-framework to manage the application flow, the Twig template engine to render page templates, and MongoDB to store the list of selected cities. The application will be hosted on IBM Bluemix and will connect to the Insights for Weather service for weather data and the GeoNames service for city information.

There are a lot of technologies in use here, so here's what you'll need:

  • A basic familiarity with jQuery Mobile and PHP.
  • A local PHP development environment with either Apache (with mod_rewrite and .htaccess file support) or NGINX.
  • A local or remote deployment of MongoDB with a configured database, user, and password. To obtain a free or paid deployment of MongoDB, register for a MongoLab account.
  • A Bluemix account (register for your free trial account or log in to Bluemix if you already have an account).
  • A free GeoNames account with web service access enabled.
  • Composer, the PHP dependency manager.
  • The CloudFoundry command-line tool.
  • A text editor or IDE.

This tutorial shows you how to use the GeoNames API and IBM Bluemix to construct a simple weather monitor for use in the cloud.

Note: Any application that uses the GeoNames web service must comply with the GeoNames Terms and Conditions. Similarly, any application that uses the IBM Insights for Weather service must comply with the service's terms of use, as described in the Bluemix documentation. Before beginning your project, spend a few minutes reading these requirements and ensuring that your application complies with them.

Step 1. Create the bare application

The first step is to initialize a basic application with the Silex PHP micro-framework and Twig templating engine.

  1. Download the Silex PHP micro-framework and the Twig templating engine using Composer, the PHP dependency manager. Use the following Composer configuration file and save it to $APP_ROOT/composer.json (where $APP_ROOT refers to your project directory).
    {
        "require": {
            "silex/silex": "*",
            "twig/twig": "*"   
        },
        "minimum-stability": "dev",
        "prefer-stable": true
    }
  2. Install Silex and Twig using Composer with the following command:
    shell> php composer.phar install
  3. Set up the main control script for the application. The following script loads the Silex framework and initializes the Silex application. It also contains callbacks for each of the application's routes, with each callback defining the code to be executed when the route is matched to an incoming request. Because the application must support listing, adding, deleting, and searching for cities, you can define the URL routes /index, /add, /delete, and /search as shown below. Note that this script should be saved as $APP_ROOT/index.php.
    <?php
    // use Composer autoloader
    require 'vendor/autoload.php';
    require 'config.php';
    
    // load classes
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Silex\Application;
    
    // initialize Silex application
    $app = new Application();
    
    // load configuration from file
    $app->config = $config;
    
    // register Twig template provider
    $app->register(new Silex\Provider\TwigServiceProvider(), array(
      'twig.path' => __DIR__.'/views',
    ));
    
    // register URL generator
    $app->register(new Silex\Provider\UrlGeneratorServiceProvider());
    
    // index page handlers
    $app->get('/', function () use ($app) {
      return $app->redirect($app["url_generator"]->generate('index'));
    });
    
    $app->get('/index', function () use ($app, $db) {
        // CODE
    })
    ->bind('index');
    
    // search form
    $app->get('/search', function () use ($app) {
        // CODE
    })
    ->bind('search');
    
    // search processor
    $app->post('/search', function (Request $request) use ($app) {
        // CODE
    });
    
    // handler to add city to database
    $app->get('/add/{gid}', function ($gid) use ($app, $db) {
        // CODE
    })
    ->bind('add');
    
    // handler to remove city from database
    $app->get('/delete/{id}', function ($id) use ($app, $db) {
        // CODE
    })
    ->bind('delete');
    
    // error page handler
    $app->error(function (\Exception $e, $code) use ($app) {
        // CODE
    });
    
    $app->run();
  4. Construct a simple page template for the various views rendered by the application. Here's an example:
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>    
      </head>
      <body>
    
        <div data-role="page">
    
          <div data-role="header">
            <h1>Weather Tracker</h1>
          </div>
    
          <div data-role="content">       
          </div>
    
          <div data-role="footer">
          </div>
    
        </div>
          
      </body>
    </html>

This page sets up jQuery and jQuery Mobile from the Google Content Delivery Network (CDN) and defines a basic jQuery Mobile page. As you can see from the code above, the page currently contains a header, a content area, and a footer. As we progress through this tutorial, this page template will be populated with content for each view.

Step 2. Implement location search

The IBM Insights for Weather service requires, as mandatory input to its API, the latitude and longitude values for the location whose weather is being requested. However, because it is unreasonable to expect users to have these values for all the locations they want to monitor, the application needs to have a way to map location names to their corresponding geographical coordinates.

That function is provided by the GeoNames geographical database, which contains data for more than 8 million locations, complete with geographical coordinates as well as population figures and features. This database is accessible using the GeoNames web service, making it easy to integrate GeoNames data into a PHP application.

  1. To use GeoNames, sign up for a free GeoNames account and then enable web service access for the account. You can then access the API by requesting a URL such as http://api.geonames.org/search?q=london&maxRows=20&username=USERNAME, which produces a set of location names matching the search query, as shown:
  2. Notice that each result includes a unique GeoNames ID, together with the latitude, longitude, city name, and country name and code. It's easy enough to use this information to implement a search interface for the application:
    <?php
    // search form
    $app->get('/search', function () use ($app) {
      return $app['twig']->render('search.twig', array());
    })
    ->bind('search');
    
    // search processor
    $app->post('/search', function (Request $request) use ($app) {
      // search for location string against Geonames database
      // for each result, store location name, country and Geonames ID
      $query = urlencode(strip_tags($request->get('query')));
      $sxml = simplexml_load_file('http://api.geonames.org/search?q=' . $query . '&maxRows=20&username=' . $app->config['geonames_uid']);
      if ($sxml === FALSE) {
        throw new Exception("Could not connect to Geonames API.");  
      }
      $data = array();
      foreach ($sxml->geoname as $g) {
        $data[] = array(
          'gid' => (int)$g->geonameId,
          'location' => (string)$g->name,
          'country' => (string)$g->countryName,
        );
      }
      return $app['twig']->render('search.twig', array('data' => $data, 'query' => urldecode($query)));
    });
  3. The first GET handler renders a simple search form containing a text input field. When the user submits this form, the second POST handler takes care of:
    • Interpolating the search query into a GeoNames web service call
    • Iterating over the result
    • Creating an array containing the GeoNames ID for the location, the location name, and the country name

    This array is then passed to the search template where it is displayed as a list. Here is the code for the search form and results template, located at $APP_ROOT/views/search.twig:

          <div data-role="content">       
            <div id="search">
              <h2 class="ui-bar ui-bar-a">Location Search</h2>
              <div class="ui-body">
                <form method="post" data-ajax="false" action="{{ app.url_generator.generate('search') }}">
                  <input type="search" name="query" value="{{ query}}" />
                  <input type="submit" name="submit" value="Search" />
                </form>
              </div>
              {% if data %}
              <h2 class="ui-bar ui-bar-a">Results</h2>
              <div class="ui-body">
                <ul data-role="listview" data-split-icon="plus" data-split-theme="d">          
                {% for item in data %}
                  <li>
                      <a>{{ item.location }}, {{ item.country }}</a>
                      <a href="{{ app.url_generator.generate('add', {'gid': item.gid}) }}" data-ajax="false">Add</a>                 
                  </li>
                {% endfor %}
                </ul>
              </div>
              {% endif %}
            </div>
          </div>

    And here's an example of what the form looks like:

Step 3. Add selected locations to the database

Before proceeding, you need to initialize a MongoDB database connection, using information sourced from the $APP_ROOT/config.php file. Here's the code fragment that does this:

<?php
// configure MongoDB client
$dbn = substr(parse_url($app->config['db_uri'], PHP_URL_PATH), 1);
$mongo = new MongoClient($app->config['db_uri'], array("connectTimeoutMS" => 30000));
$db = $mongo->selectDb($dbn);

You'll notice from the template in the previous step that each location in the search results includes an Add button, which is hyperlinked to the /add route and which passes the unique GeoNames ID for that location to the handler. Let's look at that handler next:

// handler to add location to database
$app->get('/add/{gid}', function ($gid) use ($app, $db) {
  // use Geonames ID to get location latitude/longitude from Geonames service
  // connect to MongoDB and save in database
  $collection = $db->locations;
  $query = (int)urlencode(strip_tags($gid));
  $sxml = simplexml_load_file('http://api.geonames.org/get?geonameId=' . $query . '&username=' . $app->config['geonames_uid']);
  if ($sxml === FALSE) {
    throw new Exception("Could not connect to Geonames API.");  
  }
  $location = new stdClass;
  $location->gid = trim(strip_tags($sxml->geonameId));
  $location->location = trim(strip_tags($sxml->name));
  $location->country = trim(strip_tags($sxml->countryCode));
  $location->lat = trim(strip_tags($sxml->lat));
  $location->lng = trim(strip_tags($sxml->lng));
  $cursor = iterator_to_array($collection->find());
  // disallow if 5 locations already exist
  if (count($cursor) >= 5) {
    throw new Exception("A maximum of 5 locations are supported. Please remove a location and try again.");
  }
  // disallow if selected location already exists
  foreach ($cursor as $key => $value) {
    if ($value['gid'] == $location->gid) {
      throw new Exception("The selected location already exists in the location list.");  
    }
  }
  $collection->save($location);
  return $app->redirect($app["url_generator"]->generate('index'));
})
->bind('add');

The /add route handler receives a GeoNames ID as a route parameter. It then connects to the GeoNames Web service, this time using the /get API endpoint to retrieve details of only the selected location. The information from the web service is then converted into a PHP object with properties corresponding to the location's GeoNames ID (gid), location name (location), country code (country), latitude (lat), and longitude (lng). The MongoDB connection object is then used to save this object to the MongoDB database, in a locations collection.

Notice that the /add route handler also includes two additional checks before saving the selected location to the database: It checks that the selected location doesn't already exist in the database, and it checks that the maximum number of locations in the database doesn't exceed 5. The reason for the first check should be self-evident. The reason for the second check is that the free version of the Insights for Weather service allows only a limited number of API calls, and a separate API call must be made for each latitude/longitude pair. Therefore, limiting the number of locations to 5 helps the application stay within the service quota, and also gets all the necessary weather data in faster due to the smaller number of requests needed.

Conclusion

In this part, you have seen how to create the base application and retrieve and store latitude and longitude coordinates for world cities from an online service. In Part 2, I introduce you to the new IBM Insights for Weather service in Bluemix. Then I will show you how to use the city information from Part 1 to track the weather in five world cities of your choice and finally how to host the application on IBM Bluemix.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Cloud computing, Mobile development
ArticleID=1026497
ArticleTitle=Build a weather-tracking application in the cloud, Part 1
publish-date=02022016