Contents


Real-world applications using developerWorks Premium, Part 2

Build a mobile web app for assisting stray dogs, Part 2

Use Bluemix, PHP, and developerWorks Premium benefits to help rescue agencies aid homeless animals in distress

Comments

Content series:

This content is part # of 2 in the series: Real-world applications using developerWorks Premium, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Real-world applications using developerWorks Premium, Part 2

Stay tuned for additional content in this series.

In Part 1 of this tutorial, I introduced my stray-dog assistance application and walked you through building one of its two main components: an interface for concerned citizens to use for reporting injured strays, complete with photos and GPS locations. To build this component, you used two Bluemix services —Cloudant NoSQL DB and Object Storage— and connected them with some PHP glue.

This concluding segment focuses on adding features to help streamline the work of the rescue agencies that receive the reports: search capability and map integration. With these tools, the agencies can sift efficiently through the database of submitted reports.

The new search feature has three parts:

  • A search form in which users enter search keywords or criteria
  • A search index on the Cloudant database that specifies and indexes the fields to be queried
  • A search processor in the application that applies the user-specified search criteria against the search index and returns a list of matching results

You'll add the search feature first. Then, you'll use the Google Static Maps API to integrate map functionality into the app. Finally, you'll deploy the application to Bluemix.

By combining hosted, scalable computing with object and data storage resources, developers can leverage mobile devices and cloud technology to come up with efficient ways to address certain social issues.

Run the appGet the code

1

Create a search form

Recall from Part 1 that when users submit a report about a stray, they enter identifying details such as the dog's color, age, and sex. When input is already structured in this manner, it's easier to search through it.

To build the search form, create $APP_ROOT/views/search.twig and add the following code to it:

      <div class="panel panel-default">
        <form method="post" action="{{ app.url_generator.generate('search') }}">
          <div class="panel-heading clearfix">
            <h4 class="pull-left">Search Criteria</h4>
          </div>
          <div class="panel-body">
            <div class="form-group">
              <label for="color">Color</label>
              <input type="text" class="form-control" id="color" name="color"></input>
            </div>
            <div class="form-group">
              <label for="gender">Sex</label>
              <select name="gender" id="gender" class="form-control">
                <option value="">Any</option>
                <option value="male">Male</option>
                <option value="female">Female</option>
              </select>
            </div>
            <div class="form-group">
              <label for="age">Age</label>
              <select name="age" id="age" class="form-control">
                <option value="">Any</option>
                <option value="pup">Pup</option>
                <option value="adult">Adult</option>
              </select>
            </div>
            <div class="form-group">
              <label for="keywords">Keywords (comma-separated)</label>
              <input type="text" name="keywords" id="keywords" class="form-control"></input>
            </div>
            <div class="form-group">
              <button type="submit" name="submit" class="btn btn-primary">Submit</button>
            </div>          
          </div>
        </form>
      </div>

Here's what the search form looks like in the app UI:

Screenshot of the search form
Screenshot of the search form

Users can search using multiple criteria, including color, age, sex, and keywords (with the keywords to be matched against the free-form text fields for identifying marks and description of injury in the report).

2

Create a search index on the Cloudant database

Now, add a search index on the Cloudant database:

  1. Log in to the Bluemix console and launch the dashboard for your Cloudant instance.
  2. Select the stray_assist database that you created in Part 1. On the resulting page, click the + that's next to Design Documents, and select the New Search Index option: Screenshot of the sequence for creating a new search index in Cloudant
    Screenshot of the sequence for creating a new search index in Cloudant
  3. Enter reports as the name of the design doc, and enter search as the search index name.
  4. You use JavaScript to define search indexes in Cloudant. Copy and paste the following JavaScript code into the Search index function field:
    function (doc) {
      index("id", doc._id);  
      if(doc.color) {    
        index("color", doc.color, {"store": "yes"});  
      }  
      if(doc.gender) {    
        index("gender", doc.gender, {"store": "yes"});  
      }  
      if (doc.description) {    
        index("description", doc.description, {"store": "yes"});  
      }  
      if (doc.identifiers){    
        index("identifiers", doc.identifiers, {"store": "yes"});  
      }
      if (doc.datetime){    
        index("datetime", doc.datetime, {"store": "yes"});  
      }  
      if (doc.type){    
        index("type", doc.type, {"store": "yes"});  
      }  
      if (doc.age){    
        index("age", doc.age, {"store": "yes"});  
      }
    }
  5. Click Create Document and Build Index to save the index to the system.

Now it's possible to use the search() function to retrieve documents that match the indexed fields, as you'll see in the next step.

Search indexes are a special type of design document in Cloudant, which is similar to CouchDB. In addition to learning more about Cloudant in the Cloudant documentation, you might want to read CouchDB: The Definitive Guide (O'Reilly, ISBN 978-0-596-15589-6). This book is one of the many available from Safari Books Online, included as part of your developerWorks Premium subscription.

3

Process search queries

In this step, you add the PHP code that accepts the user's search criteria and executes a query against the Cloudant index. You also update the view template to present the list of search results.

  1. In $APP_ROOT/public/index.php, add a callback that:
    1. Accepts POST submissions from the search form
    2. Sanitizes the user input
    3. Dynamically generates a Cloudant search query depending on the user-entered criteria
    4. After the search query is finalized, generates a GET request — via the Cloudant REST API — to the Cloudant search index that you constructed in Step 2

    Here's the callback code:

    <?php
    
    // Silex application initialization - snipped
    
    // search submission handler
    $app->post('/search', function (Request $request) use ($app, $guzzle) {
      // collect and sanitize inputs
      $color = strip_tags(trim($request->get('color')));
      $gender = strip_tags(trim($request->get('gender')));
      $age = strip_tags(trim($request->get('age')));
      $keywords = strip_tags(trim($request->get('keywords')));
      if (!empty($keywords)) {
        $keywords = explode(',', strip_tags($request->get('keywords')));
      }
      
      // generate query string based on inputs
      $criteria = array("(type:report)");
      if (!empty($color)) {
        $color = strtolower($color);
        $criteria[] = "(color:$color)";
      }
      if (!empty($gender)) {
        $criteria[] = "(gender:$gender)";
      }
      if (!empty($age)) {
        $criteria[] = "(age:$age)";
      }  
      if (is_array($keywords)) {
        foreach ($keywords as $keyword) {
          $keyword = trim($keyword);
          $criteria[] = "(identifiers:$keyword OR description:$keyword)";    
        }
      }
      $queryStr = implode(' AND ', $criteria);
      
      // execute query and decode JSON response
      // transfer result set to view
      $response = $guzzle->get($app->config['settings']['db']['name'] . '/_design/reports/_search/search?include_docs=true&sort="-datetime"&q='.urlencode($queryStr));
      $results = json_decode((string)$response->getBody());
      return $app['twig']->render('search.twig', array('results' => $results));
    });
    
    // other callbacks  snipped
    
    $app->run();

    The response to the search request is a JSON document containing matching results (reports). The include_docs parameter in the GET request ensures that the complete report is included in the search response, while the sort parameter takes care of sorting reports with the most recent first. The JSON document is then converted to a PHP array and returned to the view template.

  2. Add the following code block to $APP_ROOT/views/search.twig to update the view template to present the list of search results:
          {% if results %}
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Search Results</h4>
            </div>
            <div class="panel-body">
              {% for r in results.rows %}
                <div class="row">
                  <div class="col-md-8">
                    <strong>
                    {{ r.doc.color|upper }} 
                    {{ r.doc.gender != 'unknown' ? r.doc.gender|upper : '' }} 
                    {{ r.doc.gender != 'unknown' ? r.doc.age|upper : '' }}
    
                    </strong>
                    <p>{{ r.doc.description }} <br/>
                    Reported on {{ r.doc.datetime|date("d M Y H:i") }}</p>
                  </div>
                  <div class="col-md-4">
                    <a href="{{ app.url_generator.generate('detail', {'id': r.doc._id|trim}) }}" class="btn btn-primary">Details</a>
                    <a href="{{ app.url_generator.generate('map', {'id': r.doc._id|trim}) }}" class="btn btn-primary">Map</a></li>
                  </div>
                </div>
                <hr />
              {% endfor %}
            </div>
          </div>
          {% endif %}

Here's an example search result in the app UI:

Screenshot of an example search result
Screenshot of an example search result

The result view indicates the dog's color, sex, age, and injury type, and the date and time of the report. Included are buttons that link to a detail page and to a map page. Both pages include the Cloudant document identifier for the report as an additional route parameter. You'll implement the details page next.

4

Retrieve report details

The details page is mapped to the /details route. Ideally, accessing this route takes the user to a page that contains the complete details of the submitted report, including a full description of the injury, the reporter's contact information, and a photo of the injured stray (if available).

In $APP_ROOT/public/index.php, add the following code for the /details callback handler:

<?php

// Silex application initialization  snipped

// report display
$app->get('/detail/{id}', function ($id) use ($app, $guzzle, $objectstore) {
  // retrieve selected report from database
  // using unique document identifier
  $response = $guzzle->get($app->config['settings']['db']['name'] . '/_all_docs?include_docs=true&key="'. $id . '"');
  $result = json_decode((string)$response->getBody());
  if (count($result->rows)) {
    $result = $result->rows[0];
    return $app['twig']->render('detail.twig', array('result' => $result));
  } else {
    $app['session']->getFlashBag()->add('error', 'Report could not be found.');
    return $app->redirect($app["url_generator"]->generate('index'));  
  }
})
->bind('detail');

// other callbacks  snipped

$app->run();

The callback handler looks for the document identifier in the route signature and uses it to request the specified document from the Cloudant database. The query string sent to the Cloudant API uses the key parameter to restrict the result set to the single document matching that key. The result is then returned to the view script, where it's formatted for display.

The view script is $APP_ROOT/views/detail.twig. You can get the entire file from my project via the Get the code button that precedes Step 1.

The main point of interest in detail.twig is the report's photo attachment. Because files stored in the Object Storage service aren't publicly accessible via URL by default, the original photo upload can't be directly embedded in the details page. Instead, the <img> element references a proxy route, /photo. Internally, the proxy route does the hard work of connecting to the Object Storage service, retrieving the image file, and sending it to the client for display.

Here's what the callback handler for the /photo route contains:

<?php

// Silex application initialization  snipped

// photo display
$app->get('/photo/{id}/{filename}', function ($id, $filename) use ($app, $guzzle, $objectstore) {
  // retrieve image file content from object store
  // using document identifier and filename
  // guess image MIME type from extension
  $file = $objectstore->getContainer($id)
                      ->getObject($filename)
                      ->download();
  $ext = pathinfo($filename, PATHINFO_EXTENSION);  
  $mimetype = GuzzleHttp\Psr7\mimetype_from_extension($ext);             
  
  // set response headers and body
  // send to client
  $response = new Response();
  $response->headers->set('Content-Type', $mimetype);
  $response->headers->set('Content-Length', $file->getSize());
  $response->headers->set('Expires', '@0');
  $response->headers->set('Cache-Control', 'must-revalidate');
  $response->headers->set('Pragma', 'public');
  $response->setContent($file);
  return $response;
})
->bind('photo');

// other callbacks  snipped

$app->run();

The /photo callback handler accepts two parameters: the document identifier and the photo filename. The handler uses this information to connect to the Object Storage service via the OpenStack client, locate the correct container, and within the container, locate the correct image. These tasks use the client's getContainer(), getObject(), and download() methods. (Recall from Part 1 that containers are named according to their document identifier, and image file names are stored as part of each document's content in the Cloudant database.)

After the image is retrieved, the callback deduces the image MIME type from its file extension and generates a new Silex response object for the binary content of the image. The callback attaches the necessary headers so that the browser can recognize it as an image, and then it sends the complete response to the requesting client.

As in this example, the report detail page shows all of the user-entered information, including a photo:

Screenshot of the report detail page
Screenshot of the report detail page
5

Prepare to use the Google Static Maps API

Next up is map integration. Recall from Part 1 that each report is automatically geotagged with the user's current location — by implication, usually also the location of the stray being reported. The GPS coordinates of the location are stored with the other details of the report, making it possible to produce a map of the location.

The best tool for this purpose is the Google Static Maps API. You can use the API to present a map of any location, given its latitude and longitude. Before you can use the API, you must register your web application with Google:

  1. Log in to your Google account and go to the Google Developers Console.
  2. Create a new project, assign it a name, and use the API Manager to turn on access to the Google Static Maps API: Screenshot of the UI for creating a new Google Static Maps API project
    Screenshot of the UI for creating a new Google Static Maps API project

    While you're at it, familiarize yourself with the usage limits for this API.
  3. In the credentials page, make a note of the API key for public API access, which you'll use to authorize all API requests made by the application: Screenshot of the Google Static Maps API credentials page
    Screenshot of the Google Static Maps API credentials page
  4. To understand how the Google Static Maps API works, use it to produce a map of Trafalgar Square in London. Enter the following URL into your browser, replacing API_KEY with the value of your API key:
    https://maps.googleapis.com/maps/api/staticmap?key=API_KEY&size=640x480&maptype=roadmap&scale=2&markers=color:green|51.5131,-0.1221

The Static Maps API responds to the request with an image containing a map of the specified coordinates. The image size and scale are specified in the request URL, and the location is automatically highlighted with a map marker. Several other parameters are also available for controlling the map display.

6

Integrate reports with Google Maps

Now that you know how the Static Maps API works, it's easy to integrate it with the application. Add a new callback for the /map route in $APP_ROOT/public/index.php and fill it in with the following code:

<?php

// Silex application initialization snipped

// map display
$app->get('/map/{id}', function ($id) use ($app, $guzzle) {
  // retrieve selected report from database
  // using unique document identifier
  $response = $guzzle->get($app->config['settings']['db']['name'] . '/_all_docs?include_docs=true&key="'. $id . '"');
  $result = json_decode((string)$response->getBody());
  
  // obtain coordinates of report location
  // request map from Static Maps API using coordinates
  if (count($result->rows)) {
    $row = $result->rows[0];
    $latitude = $row->doc->latitude;
    $longitude = $row->doc->longitude;
    $mapUrl = 'https://maps.googleapis.com/maps/api/staticmap?key=' . $app->config['settings']['maps']['key'] . '&size=640x480&maptype=roadmap&scale=1&zoom=19
&markers=color:green|' . sprintf('%f,%f', $latitude, $longitude);
    return $app['twig']->render('map.twig', array('mapUrl' => $mapUrl, 'result' => $row));
  } else {
    $app['session']->getFlashBag()->add('error', 'Map could not be generated.');
    return $app->redirect($app["url_generator"]->generate('index'));  
  }

})
->bind('map');

// other callbacks  snipped

$app->run();

Like the /details route, the /map route callback accepts the document identifier as a route parameter and connects to Cloudant to obtain the corresponding report. The callback then extracts the saved latitude/longitude coordinates from the report and produces a Google Static Maps API request in the correct format. This request is then transferred to the corresponding view script at $APP_ROOT/views/map.twig:

<div class="panel-body">
  <img class="img-responsive" src="{{ mapUrl }}" /> <br />
  <a href="{{ app.url_generator.generate('detail', {'id': result.doc._id|trim}) }}" class="btn btn-primary">Details</a>
 </div>
</div>

Here's an example of a map produced as the result:

Screenshot of a map integrated into a report
Screenshot of a map integrated into a report
7

Deploy the app to Bluemix

At this point, the application is complete, and all that's left is to turn it on. To host the app on Bluemix:

  1. Create the application manifest file at $APP_ROOT/manifest.yml, using a unique application name (on line 3) and host name (on line 6) by appending a random string (such as your initials) to stray-assist-:
    — 
    applications:
    - name: stray-assist-<initials>
    memory: 256M
    instances: 1
    host: stray-assist-<initials>
    buildpack: https://github.com/cloudfoundry/php-buildpack.git
    stack: cflinuxfs2
  2. Configure the buildpack to use the public directory of the application as the web server directory by create a $APP_ROOT/.bp-config/options.json file with the following contents:
    {
        "WEB_SERVER": "httpd",
        "PHP_EXTENSIONS": ["bz2", "zlib", "curl", "mcrypt", "fileinfo"],
        "WEBDIR": "public",
        "PHP_VERSION": "{PHP_56_LATEST}"
    }

    To have the service instance credentials automatically sourced from Bluemix, update the code to use the Bluemix VCAP_SERVICES variable:

    <?php
    // if BlueMix VCAP_SERVICES environment available
    // overwrite local credentials with BlueMix credentials
    if ($services = getenv("VCAP_SERVICES")) {
      $services_json = json_decode($services, true);
      $app->config['settings']['db']['uri'] = $services_json['cloudantNoSQLDB'][0]['credentials']['url'];
      $app->config['settings']['object-storage']['url'] = $services_json["Object-Storage"][0]["credentials"]["auth_url"] . '/v3';
      $app->config['settings']['object-storage']['region'] = $services_json["Object-Storage"][0]["credentials"]["region"];
      $app->config['settings']['object-storage']['user'] = $services_json["Object-Storage"][0]["credentials"]["userId"];
      $app->config['settings']['object-storage']['pass'] = $services_json["Object-Storage"][0]["credentials"]["password"];  
    }
  3. Run the following CLI commands to push the application to Bluemix and bind the Cloudant and Object Storage service instances (which you created in Part 1) to it:
    cf api https://api.ng.bluemix.net
    cf login
    cf push
    cf bind-service stray-assist-[initials] "Object Storage-[id]"
    cf bind-service stray-assist-[initials] "Cloudant NoSQL DB-[id]"
    cf restage stray-assist-[initials]

You can now start using the application by browsing to the host specified in the application manifest — for example, http://stray-assist-initials.mybluemix.net.

Conclusion

This tutorial focused on prototyping a stray dog assistance application by orchestrating various Bluemix services, third-party services, and resources that are available through the developerWorks Premium program. The prototype demonstrates that by combining hosted, scalable computing with object and data storage resources, developers can leverage mobile devices and cloud technology to come up with efficient ways to address certain social issues.

I encourage you to try enhancing the stray-assist application to add more features particular to your usage scenario. Here are some ideas to get you started:

  • Support multiple photo attachments in a report.
  • Enable rescue agencies to update a report with comments or actions taken.
  • Send email notifications to report originators when strays are assisted.

Happy coding!


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=Mobile development, Web development, Cloud computing
ArticleID=1033513
ArticleTitle=Real-world applications using developerWorks Premium, Part 2: Build a mobile web app for assisting stray dogs, Part 2
publish-date=06152016