Создание пользовательской карты с помощью PHP и Google Таблиц

Памела Фокс, команда API Google Карт
Ноябрь 2007 г.

Цель

Интернет полон сообществ, объединенных географическим положением и интересами: любителей музеев, европейских соборов, государственных парков и т. д. Поэтому всегда нужен разработчик (вроде вас!), который создаст систему, где пользователи смогут добавлять геотеги мест на карту, и именно этим мы здесь и займемся. К концу этой статьи у вас будет система, в которой пользователи смогут регистрироваться, входить в систему и добавлять геотеги мест. Система будет использовать AJAX для front-end, PHP для серверных скриптов и Google Spreadsheets для хранения данных. Если вы привыкли использовать базы данных MySQL для хранения данных, вы можете легко изменить этот код, чтобы использовать бэкенд MySQL.

Эта статья разбита на следующие этапы:


Настройка электронной таблицы

Мы будем использовать Google Таблицы для хранения всех данных этой системы. Нам необходимо хранить два типа данных: информацию об учётных записях пользователей и добавленные пользователями места, поэтому мы создадим отдельный лист для каждого типа данных. Мы будем взаимодействовать с листами, используя их списочный фид, который использует первую строку листа, содержащую заголовки столбцов, и каждую последующую строку, содержащую данные.

Перейдите на сайт docs.google.com и создайте новую электронную таблицу. Переименуйте лист по умолчанию в «Пользователи» и создайте столбцы с названиями «Пользователь», «Пароль» и «Сессия». Затем добавьте ещё один лист, переименуйте его в «Местоположения» и создайте столбцы с названиями «Пользователь», «Статус», «Широта», «Долгота» и «Дата». Или, если вам не хочется тратить столько времени на ручную работу, скачайте этот шаблон и импортируйте его в Google Таблицы через меню «Файл» -> «Импорт».

Информация об учётной записи пользователя должна быть конфиденциальной (видна только вам, владельцу таблицы), в то время как добавленные пользователем места будут отображаться на общедоступной карте. К счастью, Google Таблицы позволяют выборочно определять, какие листы в таблице могут быть общедоступными, а какие должны оставаться конфиденциальными (по умолчанию). Чтобы опубликовать лист «Местоположения», перейдите на вкладку «Опубликовать», нажмите «Опубликовать сейчас», установите флажок «Автоматически переопубликовать», а затем в раскрывающемся списке «Какие части?» выберите «Только лист „Местоположения“». Правильные варианты показаны на снимке экрана ниже:

Работа с Zend GData Framework

API Google Таблиц предоставляет HTTP-интерфейс для CRUD-операций, таких как извлечение, вставка, обновление и удаление строк. Zend Framework предоставляет PHP-оболочку поверх API (и других API GData), так что вам не придётся беспокоиться о реализации сырых HTTP-операций. Zend Framework требует PHP 5.

Если у вас его ещё нет, скачайте фреймворк Zend и загрузите его на свой сервер. Фреймворк доступен здесь: http://framework.zend.com/download/gdata .

Вам следует изменить include_path в PHP, чтобы включить библиотеку Zend. Это можно сделать несколькими способами, в зависимости от уровня ваших прав администратора на сервере. Один из способов — добавить следующую строку над операторами require в любых PHP-файлах, использующих эту библиотеку:

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

Чтобы протестировать его, запустите демо-версию Spreadsheets, введя следующее в командной строке в папке demos/Zend/Gdata:

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

Если всё работает, вы увидите список своих таблиц. Если возникла ошибка, проверьте правильность пути к include и наличие установленного PHP 5.

Создание глобальных функций

Все PHP-скрипты, которые мы напишем для Карты сообщества, будут использовать общие включения, переменные и функции, которые мы поместим в один файл.

В начале файла мы будем располагать необходимыми операторами для включения и загрузки библиотеки Zend, взятыми из примера Spreadsheets-ClientLogin.php.

Затем мы определим константы, которые будут использоваться во всех файлах: ключ электронной таблицы и два идентификатора листа. Чтобы найти информацию о вашей электронной таблице, откройте её, перейдите на вкладку «Опубликовать» и нажмите «Дополнительные параметры публикации». Выберите «ATOM» в раскрывающемся списке «Формат файла» и нажмите «Сгенерировать URL». Вы увидите что-то вроде этого:

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

Ключ таблицы — это длинная буквенно-цифровая строка после «/list/», а идентификатор листа — длинная трёхсимвольная строка после неё. Чтобы узнать идентификатор другого листа, выберите его в раскрывающемся списке «Какие листы?».

Затем мы создадим три функции: setupClient, getWkshtListFeed и printFeed. В setupClient мы установим имя пользователя и пароль GMail, пройдём аутентификацию с помощью ClientLogin и вернём объект Zend_Gdata_Spreadsheets. В getWkshtListFeed мы вернём список электронных таблиц для заданного ключа и идентификатора листа, а также необязательный запрос к электронным таблицам (ссылка). Функция printFeed взята из примера Spreadsheets-ClientLogin.php и может быть полезна для отладки. Она принимает объект ленты и выводит его на экран.

PHP-код, который это делает, показан ниже ( 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++;
  }
}
 
?>

Регистрация нового пользователя

Чтобы зарегистрировать нового пользователя, нам понадобится HTML-страница, видимая пользователю, с текстовыми полями и кнопкой «Отправить», а также PHP-скрипт для добавления пользователя в электронную таблицу.

В PHP-скрипте мы сначала включаем глобальный скрипт, а затем получаем значения имени пользователя и пароля из переменной GET. Затем мы настраиваем клиент электронных таблиц и запрашиваем фид списка для листа пользователей со строкой запроса, чтобы ограничить результаты только строками, в которых столбец имени пользователя равен имени пользователя, переданному в скрипт. Если мы не получаем строк в результате фида списка, то мы можем спокойно продолжить, зная, что переданное имя пользователя уникально. Перед вставкой строки в фид списка мы создаем ассоциативный массив значений столбцов: имя пользователя, зашифрованный пароль с помощью функции PHP sha1 и символ-заполнитель для сеанса. Затем мы вызываем insertRow в клиенте электронных таблиц, передавая ассоциативный массив, ключ электронных таблиц и идентификатор листа. Если возвращенный объект — ListFeedEntry, то мы выводим сообщение Success!.

PHP-код, который это делает, показан ниже ( 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!";
}
?>

На странице регистрации мы можем включить API Карт, чтобы использовать его функцию-обёртку XMLHttpRequest под названием GDownloadUrl. Когда пользователь нажимает кнопку «Отправить», мы получаем имя пользователя и пароль из текстовых полей, формируем строку параметров на основе их значений и вызываем GDownloadUrl для URL-адреса скрипта и параметров. Поскольку мы отправляем конфиденциальную информацию, мы используем HTTP-версию POST-метода GDownloadUrl (отправляя параметры третьим аргументом, а не добавляя их к URL-адресу). В функции обратного вызова мы проверяем успешность ответа и выводим пользователю соответствующее сообщение.

Ниже показаны скриншот и код примера страницы регистрации ( 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>

Вход пользователя

Чтобы пользователи могли входить в нашу систему, нам понадобится HTML-страница, которая будет запрашивать имя пользователя и пароль, а также PHP-скрипт для проверки данных входа, создания идентификатора сеанса и его передачи на страницу входа для установки cookie-файла. На последующих страницах пользователь останется в системе благодаря cookie-файлу сеанса.

В PHP-скрипте мы сначала включаем глобальный скрипт, а затем получаем значения имени пользователя и пароля из переменной GET. Затем мы настраиваем клиент электронных таблиц и запрашиваем фид списка для листа пользователей, используя строку запроса, чтобы ограничить результаты только теми строками, в которых столбец «Имя пользователя» совпадает с именем пользователя, переданным в скрипт.

В возвращаемой строке мы проверим, совпадает ли хеш переданного пароля с хешем, сохранённым в таблице. Если это так, мы создадим идентификатор сеанса с помощью функций md5, uniqid и rand. Затем мы обновим строку в таблице, добавив номер сеанса, и выведем его на экран, если обновление строки прошло успешно.

PHP-код, который это делает, показан ниже ( 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"];
    }
  }
}
?>

На странице входа мы снова можем включить API Карт, чтобы использовать его функцию-обёртку XMLHttpRequest под названием GDownloadUrl. Когда пользователь нажимает кнопку «Отправить», мы получаем имя пользователя и пароль из текстовых полей, формируем URL-адрес скрипта с параметрами запроса и вызываем GDownloadUrl для URL-адреса скрипта. В функции обратного вызова мы устанавливаем cookie-файл с идентификатором сеанса, возвращаемым скриптом, или выводим сообщение об ошибке, если он не возвращается. Функция setCookie взята из файла cookies.js, основанного на руководстве по JavaScript от w3c: http://www.w3schools.com/js/js_cookies.asp.

Ниже показаны скриншот и код примера страницы входа ( 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>

Разрешение пользователям добавлять места на карту

Чтобы позволить пользователю добавлять места на нашу карту, нам понадобится HTML-страница, на которой пользователь сможет указать информацию о местоположении, а также два PHP-скрипта: один для проверки того, что пользователь вошел в систему с помощью установленного нами cookie-файла, а другой для добавления местоположения в рабочий лист местоположений.

В первом PHP-скрипте, проверяющем, вошёл ли пользователь в систему, мы сначала подключаем глобальный скрипт, а затем получаем значение сеанса из GET-переменной. Затем мы настраиваем клиент электронных таблиц и запрашиваем фид списка для листа пользователей, используя строку запроса, чтобы ограничить результаты только теми строками, в которых значение столбца сеанса совпадает со значением сеанса, переданным в скрипт. Затем мы перебираем пользовательские записи этого фида (те, которые соответствуют заголовкам столбцов) и выводим соответствующее имя пользователя для этого сеанса, если оно существует.

PHP-код, который это делает, показан ниже ( 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();
    }
  }
}
?>

Во втором PHP-скрипте, позволяющем пользователю добавить местоположение, мы сначала реплицируем код из communitymap_checksession.php, чтобы убедиться, что пользователь всё ещё вошел в систему и является действительным. Затем, получив корректное имя пользователя из таблицы пользователей, мы получаем значения местоположения, широты и долготы из переменной GET. Мы помещаем все эти значения в ассоциативный массив и добавляем значение «даты» с помощью функции PHP date(), чтобы узнать, когда пользователь добавил местоположение. Мы передаем этот ассоциативный массив, константу ключа таблицы и константу идентификатора листа местоположения в функцию insertRow. Затем мы выводим «Success», если строка для нового местоположения была добавлена в таблицу. Если на этом этапе возникает ошибка, вероятно, это связано с несовпадением имён заголовков столбцов. Ключи в ассоциативном массиве должны совпадать с заголовками столбцов на листе, заданными ключом таблицы и идентификатором листа.

PHP-код, который это делает, показан ниже ( 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";
  }
}

?>

На странице добавления местоположения мы снова можем включить API Карт, чтобы использовать GDownloadUrl и создать карту. После загрузки страницы мы используем функцию getCookie из cookies.js для получения значения сеанса. Если строка сеанса пуста или равна нулю, мы выводим сообщение об ошибке. Если нет, мы вызываем GDownloadUrl в map.checksession.php, отправляя данные сеанса. Если имя пользователя успешно возвращено, мы отображаем приветственное сообщение, открываем форму добавления местоположения и загружаем карту. Форма состоит из текстового поля адреса, карты и текстовых полей для названия места, широты и долготы. Если пользователь ещё не знает широту/долготу местоположения, он может геокодировать его, введя свой адрес в форму и нажав «Отправить». Это отправит вызов к GClientGeocoder API Карт, который разместит маркер на карте, если найдет адрес, и автоматически заполнит текстовые поля широты/долготы.

Когда пользователь будет удовлетворен, он может нажать кнопку «Добавить местоположение». Затем в JavaScript мы получим значения для пользователя, места, широты и долготы и отправим их скрипту communitymap_addlocation.php с помощью GDownloadUrl .

Если скрипт завершится успешно, мы выведем на экран сообщение об успешном завершении.

Ниже показаны снимок экрана и код для примера страницы добавления местоположения ( 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>

Создание карты

Поскольку на первом этапе вы сделали таблицу местоположений общедоступной, для создания карты с этими местоположениями не требуется серверного программирования. Фактически, программирование вообще не требуется. Вы можете использовать этот мастер «Таблицы -> Карта» , и он сгенерирует весь необходимый код для карты. Мастер загружает записи таблицы на страницу, добавляя тег script, указывающий на вывод JSON для фида, и определяет функцию обратного вызова, которая будет вызвана после загрузки JSON. Подробнее см. здесь .

Пример HTML-кода для этого доступен здесь: mainmap.htm . Скриншот представлен ниже:

Заключение

Надеюсь, теперь у вас есть собственная система карт, созданная пользователями и работающая на вашем сервере. В этой статье представлен базовый код, необходимый для реализации основных функций этой системы, но теперь, когда вы знакомы с библиотекой Zend Spreadsheets, вы сможете расширить её возможности в соответствии со своими потребностями. Если вы столкнулись с ошибками, помните, что для отладки можно использовать команду echo в PHP или функцию GLog.write() Map API в JavaScript. Вы также всегда можете обратиться за помощью на форумы разработчиков Maps API или Spreadsheets API .