Desenvolvimento

28 set, 2016

Crie uma aplicação web móvel para ajudar cães de rua – Parte 02

Publicidade

Na Parte o1 deste artigo, eu apresentei a minha ideia de assistência a cães abandonados e levei você através da construção de um de seus dois componentes principais: uma interface para os cidadãos interessados utilizarem para relatar estáticas de cães feridos, com fotos e localização GPS. Para construir esse componente, você usou dois serviços Bluemix – Cloudant NoSQL DB e o Object Storage – e os conectou com um pouco de PHP.

Este artigo final se concentra na adição de recursos para ajudar a simplificar o trabalho das agências de resgate que recebem os relatórios: capacidade de pesquisa e integração com o mapa. Com essas ferramentas, as agências podem analisar de forma eficiente a base de dados dos relatórios apresentados.

O novo recurso de pesquisa tem três partes:

  • Um formulário de pesquisa em que os usuários inserem palavras-chave de busca ou critérios.
  • Um índice de pesquisa no banco de dados Cloudant que especifica e indexa os campos a serem consultados.
  • Um processador de pesquisa no aplicativo que aplica os critérios de pesquisa especificados pelo usuário em relação ao índice de pesquisa e retorna uma lista de resultados correspondentes.

Você vai adicionar o recurso de busca primeiro. Então, você vai usar a API Google Static Maps para integrar a funcionalidade de mapa no aplicativo. Finalmente, você irá implantar o aplicativo no Bluemix.

Ao combinar hospedagem, computação escalável com recursos de objeto e de armazenamento de dados, os desenvolvedores podem aproveitar dispositivos móveis e tecnologia de nuvem para chegar a formas eficientes de resolver certas questões sociais.

Execute o aplicativo.

Obtenha o código.

1 – Crie um formulário de pesquisa

Lembre-se da Parte 01, quando os usuários enviam um relatório sobre um animal, eles identificam detalhes, como a cor do cão, idade e sexo. Quando a entrada já está estruturada dessa forma, é mais fácil procurar por ela.

Para construir o formulário de busca, crie $APP_ROOT/views/search.twig e adicione o seguinte código a ele:

<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>

Aqui está como se parece o formulário de busca na interface do usuário do app:

image001

Os usuários podem pesquisar utilizando vários critérios, incluindo cor, idade, sexo e palavras-chave (as palavras-chave podem ser comparadas com os campos de texto livre para a identificação de marcas e descrição de lesão no relatório).

2 – Crie um índice de pesquisa no banco de dados Cloudant

Agora, adicione um índice de pesquisa no banco de dados Cloudant:

  1. Faça o login no console Bluemix e lance o painel para a instância Cloudant.
  1. Selecione o banco de dados stray_assist que você criou na Parte 01. Na página resultante, clique no sinal + que está ao lado para Design Documents, e selecione a opção New Search Index:

image002

  1. Digite reports como o nome do design doc, e coloque search como o nome do índice de pesquisa.
  1. Você usa JavaScript para definir os índices de pesquisa no Cloudant. Copie e cole o seguinte código JavaScript no campo Search index function:
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"});  
  }
}
  1. Clique em Create Document and Build Index para salvar o índice no sistema.

Agora é possível usar a função search() para recuperar documentos que correspondem aos campos indexados, como você verá na próxima etapa.

Índices de pesquisa são um tipo especial de design document no Cloudant, que é semelhante ao CouchDB. Além de aprender mais sobre Cloudant na documentação Cloudant, você pode querer ler CouchDB: The Definitive Guide (O’Reilly, ISBN 978-0-596-15589-6). Esse livro é um dos muitos disponíveis na Safari Books Online, incluído como parte de sua assinatura no developerWorks Premium.

3 – Processos de consultas de pesquisa

Nesta etapa, você adiciona o código PHP que aceita os critérios de pesquisa do usuário e executa uma consulta em relação ao índice Cloudant. Você também atualiza o modelo da view para apresentar a lista de resultados de pesquisa.

1 – Em $APP_ROOT/public/index.php, adicione um callback que:

a) Aceita submissões POST vindas do formulário de pesquisa.

b) Limpa a entrada do usuário.

c) Dinamicamente gera uma consulta de pesquisa Cloudant dependendo dos critérios introduzidos pelo usuário.

d) Após a consulta de pesquisa ser finalizada, gera um pedido GET – por meio da API Rest Cloudant – ao índice de pesquisa Cloudant que você construiu no passo 2.

Aqui está o código do callback:

<?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();

A resposta à solicitação de pesquisa é um documento JSON contendo os resultados correspondentes (relatórios). O parâmetro include_docs no pedido GET garante que o relatório completo esteja incluído na resposta da pesquisa, enquanto o parâmetro sort cuida para que o relatório com a classificação mais recente venha primeiro. O documento JSON é então convertido em um array PHP e devolvido para o modelo da view.

2 – Adicione o seguinte bloco de código em $APP_ROOT/views/search.twig para atualizar o modelo da view para apresentar a lista de resultados da pesquisa:

{% 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 %}

Aqui está um exemplo resultante da pesquisa na interface do usuário do app:

image003

A view de resultado indica cor, sexo, idade e tipo de lesão do cão, bem como a data e a hora do relatório. Botões estão incluídos que levam para uma página de detalhe e uma página de mapa. Ambas as páginas incluem o identificador do documento Cloudant para o relatório como um parâmetro de rota adicional. Você vai implementar a página de detalhes a seguir.

4 – Recupere detalhes do relatório

A página de detalhes é mapeada para a rota /details. Idealmente, o acesso a essa rota leva o usuário para uma página que contém os detalhes completos do relatório apresentado, incluindo uma descrição completa da lesão, informações de contato de quem reportou, e uma foto do cão ferido (se disponível).

Em $APP_ROOT/public/index.php, adicione o seguinte código para o callback /details:

<?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();

O callback procura o identificador do documento na assinatura da rota e o usa para solicitar o documento especificado no banco de dados Cloudant. A query string enviada para a API Cloudant utiliza o parâmetro key para restringir o conjunto de resultados para o documento único que combina com essa chave. O resultado é então devolvido ao script de visualização view, onde ele é formatado para exibição.

O script de visualização view é o $APP_ROOT/views/detail.twig. Você pode obter o arquivo inteiro do meu projeto através do botão Get the code que precede o Passo 1.

O principal ponto de interesse em detail.twig é a foto anexada ao relatório. Como os arquivos armazenados no serviço de armazenamento de objetos não são acessíveis ao público através de URL por padrão, o upload da foto original não pode ser incorporado diretamente na página de detalhes. Em vez disso, o elemento <img> faz referência a uma rota proxy, /photo. Internamente, a rota proxy faz o trabalho duro de se conectar ao serviço Object Storage, recuperar o arquivo de imagem e enviá-lo para o cliente para exibição.

Aqui está o que o manipulador de callback para a rota /photo contém:

<?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();

O callback /photo aceita dois parâmetros: o identificador do documento e o nome do arquivo da foto. O manipulador usa essas informações para se conectar ao serviço Object Storage através do cliente OpenStack, localizar o container correto e, dentro do recipiente, localizar a imagem correta. Essas tarefas usam os métodos do cliente GetContainer(), getObject() e download() (lembre da Parte 01, em que os containers são nomeados de acordo com o seu identificador de documentos, e os nomes de arquivo de imagem são armazenados como parte do conteúdo de cada documento no banco de dados Cloudant).

Depois que a imagem é recuperada, o callback deduz que a imagem é do tipo MIME a partir da sua extensão de arquivo e gera um novo objeto de resposta Silex para o conteúdo binário da imagem. O callback atribui os cabeçalhos necessários para que o navegador possa reconhecê-lo como uma imagem e, em seguida, ele envia a resposta completa para o cliente que solicitaram.

Como neste exemplo, a página de detalhe de relatório mostra todas as informações inseridas pelo usuário, incluindo uma foto:

image004

5 – Prepare-se para usar a API Google Static Maps

Agora é a hora da integração com o mapa. Lembre da Parte 01, em que cada relatório é automaticamente geolocalizado com a localização atual do usuário – implicando geralmente que a localização do animal que está sendo relatado também seja armazenada. As coordenadas GPS da localização são armazenadas com os outros detalhes do relatório, tornando possível produzir um mapa do local.

A melhor ferramenta para isso é a API Google Static Maps. Você pode usar a API para apresentar um mapa de qualquer localização, dada a sua latitude e longitude. Antes de poder utilizar a API, você deve registrar seu aplicativo web no Google:

  1. Faça login na sua conta do Google e vá para Google Developers Console.
  1. Crie um novo projeto, atribua um nome e use o gerenciador de API para ativar o acesso à API Google Static Maps:

image005

Enquanto você estiver por aí, familiarize-se com os limites de uso para essa API.

  1. Na página de credenciais, anote a chave de API para acesso à API pública, que você vai usar para autorizar todos os pedidos de API feitos pelo aplicativo:

image006

  1. Para entender como a API Google Static Maps funciona, use-a para produzir um mapa da Trafalgar Square, em Londres. Digite a seguinte URL no seu navegador, substituindo API_KEY pelo valor da sua chave de API:

https://maps.googleapis.com/maps/api/staticmap?key=API_KEY&size=640×480&maptype=roadmap&scale=2&markers=color:green|51.5131,-0.1221

A API Google Static Maps responde ao pedido com uma imagem contendo um mapa das coordenadas especificadas. O tamanho da imagem e a escala são especificados na URL de solicitação, e a localização é automaticamente realçada com um marcador de mapa. Vários outros parâmetros também estão disponíveis para controlar a apresentação do mapa.

6 – Integre os relatórios com o Google Maps

Agora que você sabe como a API Google Static Maps funciona, é fácil integrá-la com o aplicativo. Adicione um novo callback para a rota /map no $APP_ROOT/public/index.php e preencha-o com o seguinte código:

<?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();

Como a rota /details, a rota de callback /map aceita o identificador do documento como um parâmetro de rota e se conecta a Cloudant para obter o relatório correspondente. O callback, em seguida, extrai as coordenadas salvas de latitude/longitude a partir do relatório e produz uma solicitação na API Google Static Maps no formato correto. Esse pedido é então transferido para o script de visualização view correspondente em $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>

Aqui está um exemplo de um mapa produzido como resultado:

image008

7 – Deploy do aplicativo no Bluemix

Neste ponto, o aplicativo está completo, e tudo o que resta a fazer é ligar. Para hospedar o aplicativo em Bluemix:

  1. Crie o arquivo de manifesto do aplicativo em $APP_ROOT/manifest.yml, usando um nome exclusivo para o aplicativo (na linha 3) e o nome do host (na linha 6), acrescentando uma sequência aleatória (por exemplo, suas iniciais) para 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
  1. Configure o buildpack para usar o diretório público do aplicativo como o diretório do servidor web criando um arquivo $APP_ROOT/.bp-config/options.json com o seguinte conteúdo:
{
    "WEB_SERVER": "httpd",
    "PHP_EXTENSIONS": ["bz2", "zlib", "curl", "mcrypt", "fileinfo"],
    "WEBDIR": "public",
    "PHP_VERSION": "{PHP_56_LATEST}"
}

Para ter as credenciais de instância de serviço de origem automaticamente a partir do Bluemix, atualize o código para usar a variável Bluemix VCAP_SERVICES:

<?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"];  
}
  1. Execute os seguintes comandos CLI para enviar o aplicativo para o Bluemix e vincular a Cloudant e as instâncias de Object Storage (que você criou na Parte 01) a ele:
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]

Agora você pode começar a usar o aplicativo navegando para o host especificado no manifesto do aplicativo – por exemplo, http://stray-assist-initials.mybluemix.net.

Conclusão

Este artigo focou no protótipo de um aplicativo de assistência a cães abandonados orquestrando vários serviços Bluemix, serviços de terceiros e os recursos que estão disponíveis através do programa developerWorks Premium. O protótipo demonstra que combinando hospedagem, computação escalável com recursos de objeto e de armazenamento de dados, os desenvolvedores podem aproveitar dispositivos móveis e tecnologia de nuvem para chegar a formas eficientes para resolver certas questões sociais.

Quero encorajar vocês a tentar melhorar a aplicação e ajudar a adicionar recursos específicos ao seu cenário de uso. Aqui estão algumas ideias para você começar:

  • Aceitar vários anexos de fotos em um relatório.
  • Habilitar agências de resgate para atualizar um relatório com comentários ou ações tomadas.
  • Enviar notificações por e-mail para avisar as pessoas que relataram sobre quando os animais são assistidos.

Boa programação.

Recursos para download.

Tópicos relacionados

***

Vikram Vaswani faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://www.ibm.com/developerworks/library/mo-assist-stray-dogs-2-premium/.