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.
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:
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:
- Faça o login no console Bluemix e lance o painel para a instância Cloudant.
- 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:
- Digite reports como o nome do design doc, e coloque search como o nome do índice de pesquisa.
- 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"}); } }
- 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:
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:
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:
- Faça login na sua conta do Google e vá para Google Developers Console.
- Crie um novo projeto, atribua um nome e use o gerenciador de API para ativar o acesso à API Google Static Maps:
Enquanto você estiver por aí, familiarize-se com os limites de uso para essa API.
- 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:
- 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:
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:
- 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
- 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"]; }
- 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
- Cloudant search index
- Cloudant query
- Google Static Maps API documentation
- Debugging PHP Errors on IBM Bluemix
***
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/.