Como criar um mapa enviado por usuários com PHP e Planilhas Google

Pamela Fox, equipe da API Google Maps
Novembro de 2007

Objetivo

A Web está cheia de comunidades centradas em regiões geográficas e interesses: pessoas que amam museus, catedrais europeias, parques estaduais etc. Portanto, sempre há necessidade de um desenvolvedor (como você!) para criar um sistema em que os usuários possam contribuir com lugares geotagged em um mapa, e é exatamente isso que vamos fazer aqui. No final deste artigo, você terá um sistema em que os usuários podem se registrar, fazer login e adicionar lugares com geotag. O sistema usará AJAX no front-end, PHP no script do servidor e o Google Spreadsheets no armazenamento. Se você estiver acostumado a usar bancos de dados MySQL para armazenamento, poderá modificar facilmente o código aqui para usar um back-end de banco de dados MySQL.

Este artigo está dividido nas seguintes etapas:


Como configurar o Google Spreadsheet

Vamos usar as Planilhas Google para armazenar todos os dados desse sistema. Há dois tipos de dados que precisamos armazenar: informações da conta do usuário e lugares adicionados pelo usuário. Por isso, vamos criar uma planilha para cada tipo de dado. Vamos interagir com as planilhas usando o feed de lista delas, que depende da primeira linha de uma planilha com rótulos de coluna e de cada linha subsequente com dados.

Acesse docs.google.com e crie uma planilha. Renomeie a planilha padrão como "Users" e crie colunas chamadas "user", "password" e "session". Em seguida, adicione outra planilha, renomeie como "Locations" e crie colunas chamadas "user", "status", "lat", "lng" e "date". Ou, se você não quiser fazer todo esse trabalho manual, baixe este modelo e importe para as Planilhas Google usando o comando Arquivo->Importar.

As informações da conta do usuário precisam ser mantidas privadas (visíveis apenas para o proprietário da planilha, você), enquanto os locais adicionados pelo usuário serão exibidos em um mapa visível publicamente. Felizmente, o Google Spreadsheets permite que você decida de maneira seletiva quais folhas em uma planilha podem ser públicas e quais devem permanecer privadas (padrão). Para publicar a planilha "Locais", clique na guia "Publicar", em "Publicar agora", marque a caixa de seleção "Republicar automaticamente" e, no menu suspenso "Quais partes?", selecione "Somente a planilha "Locais"". As opções corretas são exibidas na captura de tela abaixo:

Como trabalhar com o Zend Gdata Framework

A API do Google Spreadsheets fornece uma interface HTTP para operações CRUD, como a recuperação, inserção, atualização e exclusão de linhas. O Zend Framework fornece um wrapper PHP na API (e nas outras APIs GData) para que você não precise se preocupar com a implementação das operações HTTP brutas. O Zend Framework exige PHP 5.

Se você ainda não tiver, faça o download do framework Zend e envie para o servidor. O framework está disponível aqui: http://framework.zend.com/download/gdata.

Modifique o include_path do PHP de modo a incluir a biblioteca do Zend. Há diversas maneiras de fazer isso, dependendo do nível dos direitos de administração que você possui no seu servidor. Uma maneira é adicionar esta linha acima das instruções de exigência em qualquer arquivo PHP que use a biblioteca:

ini_set("include_path", ".:/usr/lib/php:/usr/local/lib/php:../../../library/");

Para testar, execute a demonstração do Google Spreadsheets inserindo isto na linha de comando na pasta demos/Zend/Gdata:

php Spreadsheet-ClientLogin.php --user=YourGMailUsername --pass=YourPassword

Se funcionar, você verá uma lista das suas planilhas. Se você receber um erro, verifique se o seu caminho de inclusão está definido corretamente e se tem o PHP 5 instalado.

Como criar funções globais

Todos os scripts PHP que vamos escrever para o mapa da comunidade usarão inclusões, variáveis e funções comuns, que vamos colocar em um arquivo.

No início do arquivo, teremos as instruções necessárias para incluir e carregar a biblioteca Zend, extraídas do exemplo "Spreadsheets-ClientLogin.php".

Em seguida, vamos definir as constantes que serão usadas em todos os arquivos: a chave da planilha e os dois IDs das planilhas. Para encontrar as informações da sua planilha, abra-a, clique na guia "Publicar" e em "Mais opções de publicação". Selecione "ATOM" na lista suspensa "Formato do arquivo" e clique em "Gerar URL". Você vai ver algo como:

http://spreadsheets.google.com/feeds/list/o16162288751915453340.4016005092390554215/od6/public/basic

A chave da planilha é a longa string alfanumérica depois de "/list/", e o ID da planilha é a string de três caracteres depois disso. Para encontrar o outro ID da planilha, selecione a outra página no menu suspenso "Quais planilhas?".

Em seguida, vamos criar três funções: setupClient, getWkshtListFeed e printFeed. Em setupClient, vamos definir nosso nome de usuário e senha do Gmail, autenticar com ClientLogin e retornar um objeto Zend_Gdata_Spreadsheets. Em getWkshtListFeed, vamos retornar um feed de lista de planilhas para uma determinada chave de planilha e ID de planilha, com uma consulta opcional de planilhas (link). A função printFeed é retirada do exemplo Spreadsheets-ClientLogin.php e pode ser útil para depuração. Ela usará um objeto de feed e o imprimirá na tela.

O PHP que faz isso é mostrado abaixo (communitymap_globals.php):

<?php
ini_set("include_path", ".:/usr/lib/php:/usr/local/lib/php:../../../library/");
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata');
Zend_Loader::loadClass('Zend_Gdata_ClientLogin');
Zend_Loader::loadClass('Zend_Gdata_Spreadsheets');
Zend_Loader::loadClass('Zend_Http_Client');

define("SPREADSHEET_KEY", "o16162288751915453340.4016005092390554215");
define("USER_WORKSHEET_ID", "od6");
define("LOC_WORKSHEET_ID", "od7");
 
function setupClient() {
  $email = "your.name@gmail.com";
  $password = "yourPassword";
  $client = Zend_Gdata_ClientLogin::getHttpClient($email, $password,
          Zend_Gdata_Spreadsheets::AUTH_SERVICE_NAME);
  $gdClient = new Zend_Gdata_Spreadsheets($client);
  return $gdClient;
}
 
function getWkshtListFeed($gdClient, $ssKey, $wkshtId, $queryString=null) {
  $query = new Zend_Gdata_Spreadsheets_ListQuery();
  $query->setSpreadsheetKey($ssKey);
  $query->setWorksheetId($wkshtId);
  if ($queryString !== null) {
    $query->setSpreadsheetQuery($queryString);
  }
  $listFeed = $gdClient->getListFeed($query);
  return $listFeed;
}
 
function printFeed($feed)
{
  print "printing feed";
  $i = 0;
  foreach($feed->entries as $entry) {
      if ($entry instanceof Zend_Gdata_Spreadsheets_CellEntry) {
         print $entry->title->text .' '. $entry->content->text . "\n";
      } else if ($entry instanceof Zend_Gdata_Spreadsheets_ListEntry) {
         print $i .' '. $entry->title->text .' '. $entry->content->text . "\n";
      } else {
         print $i .' '. $entry->title->text . "\n";
      }
      $i++;
  }
}
 
?>

Como registrar um novo usuário

Para registrar um novo usuário, vamos precisar de uma página HTML voltada para o usuário com campos de texto e um botão de envio, além de um script de back-end em PHP para adicionar o usuário à planilha.

No script PHP, incluímos primeiro o script global e, em seguida, obtemos os valores de nome de usuário e senha da variável GET. Depois, configuramos um cliente do Google Spreadsheets e solicitamos o feed de lista da planilha de usuários com uma string de consulta para restringir os resultados apenas para as linhas nas quais a coluna nome de usuário seja igual ao nome de usuário informado no script. Se não obtivermos linhas no resultado do feed de lista, poderemos continuar com segurança sabendo que o nome de usuário informado é exclusivo. Antes de inserir uma linha no feed de lista, criamos uma matriz associativa dos valores das colunas: o nome de usuário, uma criptografia da senha usando a função sha1 do PHP e um caractere de preenchimento para a sessão. Em seguida, chamamos insertRow no cliente do Google Spreadsheets, passando a matriz associativa, a chave da planilha e o ID da planilha. Se o objeto retornado for um ListFeedEntry, vamos mostrar uma mensagem de sucesso.

O PHP que faz isso é mostrado abaixo (communitymap_newuser.php):

<?php
 
require_once 'communitymap_globals.php';
 
$username = $_GET['username'];
$password = $_GET['password'];
 
$gdClient = setupClient();
 
$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('user='.$username));
$totalResults = $listFeed->totalResults;
if ( $totalResults != "0") {
  // Username already exists
  exit;
}
 
$rowArray["user"] = $username;
$rowArray["password"] = sha1($password);
$rowArray["session"] = "a";
 
$entry = $gdClient->insertRow($rowArray, SPREADSHEET_KEY, USER_WORKSHEET_ID);
if ($entry instanceof Zend_Gdata_Spreadsheets_ListEntry) {
  echo "Success!";
}
?>

Na página de registro, podemos incluir a API Maps para usar a função wrapper XMLHttpRequest chamada GDownloadUrl. Quando o usuário clica no botão "Enviar", recebemos o nome de usuário e a senha dos campos de texto, construímos uma string de parâmetros com base nos valores e chamamos GDownloadUrl no URL e nos parâmetros do script. Como estamos enviando informações sensíveis, usamos a versão HTTP POST de GDownloadUrl (enviando os parâmetros como o terceiro argumento em vez de anexá-los ao URL). Na função de callback, vamos verificar se a resposta foi bem-sucedida e mostrar uma mensagem adequada ao usuário.

Confira abaixo uma captura de tela e o código de uma página de registro de exemplo (communitymap_register.htm):


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  <title> Community Map - Register/Login </title>

  <script src="http://maps.google.com/maps?file=api&v=2&key=abcdef"
      type="text/javascript"></script>
  <script type="text/javascript">

  function register() {
    var username = document.getElementById("username").value;
    var password = document.getElementById("password").value;
    var url = "communitymap_newuser.php?";
    var params = "username=" + username + "&password=" + password;
    GDownloadUrl(url, function(data, responseCode) {
      if (data.length > 1) {
        document.getElementById("message").innerHTML = "Successfully registered." + 
          "<a href='communitymap_login.htm'>Proceed to Login</a>.";
      } else {
        document.getElementById("message").innerHTML = "Username already exists. Try again.";
      }
    }, params);
  }

  </script>

  </head>
  <body>
  <h1>Register for Community Map</h1>
  <input type="text" id="username">
  <input type="password" id="password">

  <input type="button" onclick="register()" value="Register">
  <div id="message"></div>
  </body>
</html>

Como fazer login de um usuário

Para permitir que os usuários façam login no nosso sistema, vamos precisar de uma página HTML voltada para o usuário que solicite o nome de usuário e a senha, além de um script PHP para verificar as informações de login, criar um ID de sessão e transmiti-lo de volta à página de login para definir um cookie. O usuário permanecerá conectado por meio do cookie da sessão nas páginas subsequentes.

No script PHP, incluímos primeiro o script global e, em seguida, obtemos os valores de nome de usuário e senha da variável GET. Depois, configuramos um cliente do Google Spreadsheets e solicitamos o feed de lista da planilha de usuários com uma string de consulta para restringir os resultados apenas para as linhas nas quais a coluna nome de usuário seja igual ao nome de usuário informado no script.

Na linha retornada, vamos verificar se o hash da senha transmitida corresponde ao hash armazenado na planilha. Se for, vamos criar um ID de sessão usando as funções md5, uniqid e rand. Em seguida, vamos atualizar a linha na planilha com a sessão e mostrar na tela se a atualização for bem-sucedida.

O PHP que faz isso é mostrado abaixo (communitymap_loginuser.php):

<?php
 
require_once 'communitymap_globals.php';
 
$username = $_POST['username'];
$password = $_POST['password'];
 
$gdClient = setupClient();
 
$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('user='.$username));
 
$password_hash = sha1($password);
$row = $listFeed->entries[0];
$rowData = $row->getCustom();
foreach($rowData as $customEntry) {
  if ($customEntry->getColumnName()=="password" && $customEntry->getText()==$password_hash) {
    $updatedRowArray["user"] = $username;
    $updatedRowArray["password"] = sha1($password);
    $updatedRowArray["session"] = md5(uniqid(rand(), true));
    $updatedRow = $gdClient->updateRow($row, $updatedRowArray); 
    if ($updatedRow instanceof Zend_Gdata_Spreadsheets_ListEntry) {
      echo $updatedRowArray["session"];
    }
  }
}
?>

Na página de login, podemos incluir novamente a API Maps para usar a função wrapper XMLHttpRequest chamada GDownloadUrl. Quando o usuário clica no botão "Enviar", recebemos o nome de usuário e a senha dos campos de texto, construímos o URL do script com os parâmetros de consulta e chamamos GDownloadUrl no URL do script. Na função de callback, vamos definir um cookie com o ID da sessão retornado pelo script ou mostrar uma mensagem de erro se nenhum for retornado. A função "setCookie" vem de um arquivo cookies.js baseado no tutorial de JavaScript da W3C: http://www.w3schools.com/js/js_cookies.asp.

Confira abaixo uma captura de tela e o código de uma página de login de exemplo (communitymap_login.htm):


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>

  <title>Community Map - Login</title>
      <script src="http://maps.google.com/maps?file=api&v=2&key=abcdef"
      type="text/javascript"></script>
  <script src="cookies.js" type="text/javascript"></script>
  <script type="text/javascript">

  function login() {
    var username = document.getElementById("username").value;
    var password = document.getElementById("password").value;
    var url = "communitymap_loginuser.php?username=" + username + "&password=" + password;
    GDownloadUrl(url, function(data, responseCode) {
      if (data.length > 1) {
        setCookie("session", data, 5);
      } else {
        document.getElementById("nessage").innerHTML = "Error. Try again.";
      }
    });
  }

  </script>
  </head>
  <body>

  <h1>Login for Community Map</h1>

  <input type="text" id="username">
  <input type="password" id="password">
  <input type="button" onclick="login()" value="Login">
  <div id="message"></div>
  </body>
</html>

Como permitir que os usuários adicionem locais ao mapa

Para permitir que um usuário adicione lugares ao nosso mapa, vamos precisar de uma página HTML voltada para o usuário para que ele forneça informações sobre o local e dois scripts PHP: um para verificar se ele fez login usando o cookie que definimos e outro para adicionar o local à planilha de locais.

No primeiro script PHP que verifica se um usuário está conectado, incluímos primeiro o script global e depois obtemos o valor da sessão da variável GET. Em seguida, configuramos um cliente do Google Spreadsheets e solicitamos o feed de lista para a planilha de usuários com uma string de consulta para restringir os resultados apenas para as linhas nas quais a coluna sessão seja igual ao valor da sessão informado no script. Depois, acessamos as entradas personalizadas desse feed (aquelas que correspondem aos cabeçalhos das nossas colunas) e imprimimos o nome de usuário correspondente para essa sessão, se existir.

O PHP que faz isso é mostrado abaixo (communitymap_checksession.php):

<?php

require_once 'communitymap_globals.php';

$session = $_GET['session'];

$gdClient = setupClient();

$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('session='.$session));

if ( count($listFeed->entries) > 0) {
  $row = $listFeed->entries[0];
  $rowData = $row->getCustom();
  foreach($rowData as $customEntry) {
    if ($customEntry->getColumnName()=="user") {
      echo $customEntry->getText();
    }
  }
}
?>

No segundo script PHP, que permite ao usuário adicionar um local, primeiro copiamos o código de communitymap_checksession.php, para garantir que o usuário ainda esteja conectado e válido. Em seguida, após obtermos um nome de usuário válido da planilha de usuários, obtemos os valores de local, latitude e longitude da variável GET. Colocamos todos esses valores em uma matriz associativa e também adicionamos um valor "date" usando a função date() do PHP para saber quando o usuário adicionou o lugar. Passamos essa matriz associativa, a constante de chave das planilhas e a constante do id da planilha de locais para a função insertRow. Em seguida, vamos mostrar "Sucesso" se uma linha para o novo local for adicionada à planilha. Se você receber um erro nessa etapa, provavelmente é devido a uma incompatibilidade nos nomes dos cabeçalhos das colunas. As chaves na matriz associativa devem corresponder aos cabeçalhos das colunas na planilha especificada pela chave da planilha e pelo ID da folha.

O PHP que faz isso é mostrado abaixo (communitymap_addlocation.php):

<?php

require_once 'communitymap_globals.php';

$session = $_GET['session'];

$gdClient = setupClient();

$listFeed = getWkshtListFeed($gdClient, SPREADSHEET_KEY, USER_WORKSHEET_ID, ('session='.$session));

if ( count($listFeed->entries) > 0) {
  $row = $listFeed->entries[0];
  $rowData = $row->getCustom();
  foreach($rowData as $customEntry) {
    if ($customEntry->getColumnName()=="user") {
      $user = $customEntry->getText();
    }
  }

  $place = $_GET['place'];
  $lat = $_GET['lat'];
  $lng = $_GET['lng'];
  $rowArray["user"] = $user;
  $rowArray["place"] = $place;
  $rowArray["lat"] = $lat;
  $rowArray["lng"] = $lng;
  $rowArray["date"] = date("F j, Y, g:i a");
  $entry = $gdClient->insertRow($rowArray, SPREADSHEET_KEY, LOC_WORKSHEET_ID);
  if ($entry instanceof Zend_Gdata_Spreadsheets_ListEntry) {
    echo "Success!\n";
  }
}

?>

Na página para adicionar um local, novamente podemos incluir a API do Google Maps para que possamos usar o GDownloadUrl e criar um mapa. Após o carregamento da página, usamos a função getCookie do cookies.js para recuperar o valor da sessão. Se a string da sessão for nula ou vazia, nós mostramos uma mensagem de erro como resultado. Caso contrário, chamamos GDownloadUrl em map.checksession.php, enviando a sessão. Se isso retornar um nome de usuário, nós exibimos uma mensagem de boas-vindas ao usuário, revelamos o formulário para adicionar um local e carregamos o mapa. O formulário é composto de um campo de texto de endereço, mapa e campos de texto para o nome do local, latitude e longitude. Se o usuário ainda não souber a latitude/longitude do local, ele poderá fazer a geocodificação inserindo o endereço no formulário e pressionando "Enviar". Isso vai enviar uma chamada para o GClientGeocoder da API Maps, que vai colocar um marcador no mapa se encontrar o endereço e preencher automaticamente os campos de texto de latitude/longitude.

Quando o usuário estiver satisfeito, ele poderá pressionar o botão "Adicionar local". Em seguida, no JavaScript, vamos receber os valores de usuário, lugar, lat e lng e enviá-los ao script communitymap_addlocation.php com GDownloadUrl.

Se o script retornar sucesso, vamos mostrar uma mensagem de sucesso na tela.

Confira abaixo uma captura de tela e um código de uma página de exemplo para adicionar um local (communitymap_addlocation.htm):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
    <title>Community Map - Add a Place!</title>

    <script src="http://maps.google.com/maps?file=api&v=2.x&key=abcdef" type="text/javascript"></script>
    <script src="cookies.js" type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[

    var map = null;
    var geocoder = null; 
    var session = null;

    function load() {
      session = getCookie('session');
      if (session != null && session != "") {
        url = "communitymap_checksession.php?session=" + session;
        GDownloadUrl(url, function(data, responseCode) {
          if (data.length > 0) {
            document.getElementById("message").innerHTML = "Welcome " + data;
            document.getElementById("content").style.display = "block";
            map = new GMap2(document.getElementById("map"));
            map.setCenter(new GLatLng(37.4419, -122.1419), 13);
            geocoder = new GClientGeocoder();
          }
        });
      } else {
        document.getElementById("message").innerHTML = "Error: Not logged in.";
      }
    }

    function addLocation() {
      var place = document.getElementById("place").value;
      var lat = document.getElementById("lat").value;
      var lng = document.getElementById("lng").value;

      var url = "communitymap_addlocation.php?session=" + session + "&place=" + place +
                "&lat=" + lat + "&lng=" + lng;
      GDownloadUrl(url, function(data, responseCode) {
        GLog.write(data);
        if (data.length > 0) {
          document.getElementById("message").innerHTML = "Location added.";
        }
      });
    }

    function showAddress(address) {
      if (geocoder) {
        geocoder.getLatLng(
          address,
          function(point) {
            if (!point) {
              alert(address + " not found");
            } else {
              map.setCenter(point, 13);
              var marker = new GMarker(point, {draggable:true});
              document.getElementById("lat").value = marker.getPoint().lat().toFixed(6);
              document.getElementById("lng").value = marker.getPoint().lng().toFixed(6);

              map.addOverlay(marker);
              GEvent.addListener(marker, "dragend", function() {
                document.getElementById("lat").value = marker.getPoint().lat().toFixed(6);
                document.getElementById("lng").value = marker.getPoint().lng().toFixed(6);
	      });
            }
          }
        );
      }
    }
    //]]>

    </script>

  </head>

  <body onload="load()" onunload="GUnload()">
   <div id="message"></div>
   <div id="content" style="display:none">

   <form action="#" onsubmit="showAddress(this.address.value); return false">
        <p>
        <input type="text" size="60" name="address" value="1600 Amphitheatre Pky, Mountain View, CA" />
        <input type="submit" value="Geocode!" />

    </form>
      </p>

      <div id="map" style="width: 500px; height: 300px"></div>
 
 	Place name: <input type="text" size="20" id="place" value="" />
	<br/>
 	Lat: <input type="text" size="20" id="lat" value="" />
	<br/>

 	Lng: <input type="text" size="20" id="lng" value="" />

        <br/>
	<input type="button" onclick="addLocation()" value="Add a location" />
    </form>
    </div>

  </body>
</html>

Como criar o mapa

Como você tornou a planilha de locais pública na primeira etapa, não há necessidade de programação no lado do servidor para criar um mapa dos locais. Na verdade, não há necessidade de programação nenhuma. Você pode usar o Assistente de planilhas -> Mapa, que vai gerar todo o código necessário para o mapa. O assistente fará o download das entradas da planilha na página anexando uma tag de script que aponte para a saída JSON do feed e especifique uma função de retorno de chamada que é chamada assim que o download do JSON for concluído. Confira mais informações neste link.

Um exemplo de código HTML para fazer isso está disponível aqui: mainmap.htm. Uma captura de tela é exibida abaixo:

Conclusão

Agora, você já deve ter o seu próprio sistema de mapas com colaboração do usuário em execução no seu servidor. Este artigo fornece o código básico necessário para os aspectos essenciais desse sistema. No entanto, agora que você já conhece a biblioteca Zend Spreadsheets, é possível estender o sistema para atender às suas necessidades específicas. Se você encontrar erros ao longo do caminho, lembre-se de que é possível usar o comando echo em PHP ou o GLog.write() da API Maps em JavaScript para depuração. Além disso, você sempre pode postar nos fóruns para desenvolvedores da API Maps ou da API Google Sheets para receber mais ajuda.