Версия для печати


Интерактивные карты Google
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1410

Максим Мазитов
дата публикации 16-08-2009 04:08

Интерактивные карты Google

В предыдущих опусах API статических карт Google и Google Maps API. Геокодирование я рассматривал варианты картографии, ограниченные во взаимодействии с пользователем.

В этой статье я попытаюсь расписать построение приложения на Delphi 7, работающего с интерактивными гуглокартами. Используемая здесь информация находится на стыке HTML, JavaScript, ActiveX и Delphi. Я заранее не претендую на полноту и высокую точность описываемого предмета. В данной области танцев с бубнами хватит на всех шаманов Сибири. Возможно, кому-нибудь статья, а может быть, и коды, помогут обойти те проблемы, с которыми я столкнулся. Но и я решил не все.

Краткая история вопроса

Вам требуется включить картографию в приложение. И какой бюджет у вашего проекта? 0? Тогда первый вариант — берем растровую карту, привязываем по трем точкам, немного нетривиальных математических вычислений и вы в состоянии выводить требуемые точки по координатам. Отметаем сразу. Второй вариант — качаем набор компонентов, например NGis (описан на этом сайте). Качаем MapInfo, GpsEdit (есть его исходники!). Идем на www.freemaps.ru. Убиваем вагон времени на конвертацию и сведение слоев. Результат работает, но для коммерческого продукта выглядит не слишком презентабельно. И сразу же возникает вопрос актуальности карт.

У вас есть $500 на карты? Ну что же, есть вариант — (контору называть не буду) ActiveX компонент, HASP ключ, без которого компонент не работает и отдельные деньги за карту каждого региона. Цена экземпляра софта возрастает на стоимость ключа и карт. Есть вариант взлома, как же без него, но если вы собираетесь работать серьезно и распространять легальный софт, то вариант взлома не проходит.

Вы готовы выложить $5000 — отлично! Именно столько стоили все слои Питера в формате MapInfo пару-тройку лет назад. У вас есть права на карты, вы их легко распространяете (закодировав конечно, кто захочет делиться пятью тысячами уёв?). Внимание, вопрос: а существует ли актуальная карта Петербурга, не говоря уже о Москве? Сколько будут стоить обновления? Богатые конторы, конечно, могут себе позволить, хотя и они сейчас экономят. Но для старта проекта без солидной финансовой поддержки этот вариант не проходит.

В нашем случае мы выбрали вариант за $500 и реализовали его. Когда в нете появилась разрозненная информация по возможности встраивания карт с источников web-картографии, естественно, мы за неё ухватились.

Приступим

Чего мы хотим добиться: в приложении Delphi вывести интерактивную карту Google со всеми возможностями пользовательского интерфейса (масштабирование, центрирование, перетаскивание) и отобразить на карте произвольный набор точек.

Кратко схема взаимодействия выглядит так: мы встраиваем TWebBrowser в приложение, открываем HTML страницу, на ней расположен Java-applet Google Maps, которым мы управляем через вызовы JavaScript.

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

И еще: вся инфа по аплету карты почерпнута из http://code.google.com/intl/ru/apis/maps/documentation/index.html.

Немного HTML и Java Script

Качаем пример, распаковываем и смотрим каталог HTML.

Небольшое замечание — приведенные примеры работают только с Internet Explorer 7 и выше. При попытке работать в связке Delpi7-IE6 возникли проблемы, которые от недостатка времени решать даже не пытались.

Список файлов каталога HTML:

map-article-1-simple-map.htmlСамой простой вариант страницы с картой Google.
map-article-2-map-with-controls.htmlКарта с добавленными элементами управления.
map-article-3-map-with-marker.htmlКарта с примером добавления маркеров на карту.
47.pngКартинка для пользовательского маркера.
tlabel.2.05.jsРеализация TLabel (см. ниже)
map-final.htmlРеализация для использования в программе.

Все страницы подробно прокомментированы.

Откроем в текстовом редакторе страницу "map-article-3-map-with-marker.html".

В теге Body в событии загрузки страницы вызывается функция initialize, которая производит действия по созданию и настройке карты. Читаем комментарии.

Производимые действия:

  1. Создание экземпляра класса GMap2.
  2. Центрирование на требуемой координате.
  3. Создание элементов управления картой.
  4. Экспериментальный вывод точек на карте.

Ещё в теге скрипта имеются функции добавления маркеров:

function addMarker(latitude, longitude, hint)добавление пустого маркера с точкой и подсказкой
function addMarkerIcon(latitude, longitude, hint, iconPath)добавление маркера с подсказкой и пользовательской иконкой

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

Пришлось посерфить в нете и в результате, я нашел устраивающий меня код на JScript, размещающий прямоугольник с текстом в указанной географической точке. Смотрим файл "tlabel.2.05.js".

В нем реализовывается класс TLabel и добавляется функционал к классу GMap2, в котором добавляются и удаляются экземпляры класса TLabel. В меру своих скромных возможностей, я добавил метод по удалению TLabel по идентификатору. Но вот профессионального пояснения по коду скрипта дать не могу, хоть и понимаю, как он работает (сам такое написать не смогу).

Для добавления на страницу нового маркера пишем следующий код в HTML на JavaScript (см. файл "map-final.html"):

// создание маркера
function createTLabel(id, point, html, astyle) {
  // создаем экземпляр
  var label = new TLabel();
  // идентификатор
  label.id = id;
  // координаты
  label.anchorLatLng = point;
  // расположение относительно координат
  label.anchorPoint = 'center';
  // содержимое
  label.content = '<div class="'+astyle+'"><nobr><b>'+html+'</b></nobr></div>';
  // прозрачность
  label.percentOpacity = 70;
  // добавляем на карту
  map.addTLabel(label);
}

Маркер использует стили, описанные в начале файла "map-final.html". Стили завязаны на строковые названия цветов. Если возникнет надобность в новом цвете достаточно в тексте страницы описать стиль с именем формата ".<имя цвета>label".

Вызов функции добавления маркера на JavaScript:

createTLabel("Id0", new GLatLng(59.944265, 30.319948), "<nobr>Текст маркера</nobr>", "bluelabel");

Первый параметр — уникальный идентификатор, указывая который возможно удалить маркер с карты. Далее идут координаты в десятичных долях градуса, HTML текст маркера и его стиль.

Вроде бы ничего сложного.

Подключаем к Delphi

Основной вопрос — как управлять Java аплетом Google. Берем TWebBrowser и смотрим его интерфейсы. Мы можем заставить IE выполнить код JavaScript из внешнего приложения следующим образом (на этом сайте он тоже описан):

try
  if WebBrowser.Document<>nil then
      IHTMLDocument2(WebBrowser.Document).parentWindow.execScript(Script,'JavaScript');
  except
    on E:Exception do begin
      MessageBox(Handle,PChar(E.Message),'Ошибка исполнения', MB_OK or MB_ICONERROR);
    end;
  end;

, где переменная Script содержит текст скрипта. И этого достаточно.

Дальше остается только кодирование и чтение доков гугла. Смотрим исходные коды.

Все исходники, работающие с картографией, расположены в каталоге "MapsFrames".

Я использовал фреймы (TFrame) вместо компонентов по причине более легкой переносимости кода с компьютера на компьютер — их не надо регистрировать, а написать один раз 5 строк кода по встраиванию фрейма в форму ненапрягающая задача.

Класс точки TMapPoint является простым контейнером свойств точки карты — координаты, текст и т.п.

Тип TPointsChangingType описывает действия, которые производились с точками, для передачи форме в соответствующем событии.

Класс TBaseMapFrame является контейнером точек, позволяющим добавлять, изменять и удалять точки на карте, и содержит абстрактные методы по работе с картой для перекрытия их (методов) в реализациях под конкретную картографию. Это различные варианты масштабирования, а также показ и скрытие точек. При создании класса я применил Doc-View схему (не до конца, правда), поэтому фрейм имеет событие PointsChangingEvent, уведомляющее об изменениях в коллекции точек. По большому счету, это просто обертка над TObjectList с небольшим количеством абстрактных методов.

Теперь о TGoogleMapFrame. Перекрываем методы масштабирования:

// приближение карты
procedure TGoogleMapFrame.ZoomIn;
begin
  ExecScript('map.zoomIn();');
end;

// отдаление карты
procedure TGoogleMapFrame.ZoomOut;
begin
  ExecScript('map.zoomOut();');
end;

Центрирование:

// центрирование карты
procedure TGoogleMapFrame.SetCenter(Latitude:Double;Longitude:Double;Scale:Integer=-1);
var
  Script:String;
begin
  // если при вызове указан масштаб
  if Scale>=0 then begin
    Script:='var point = new GLatLng('+FloatToStr(Latitude)+','+FloatToStr(Longitude)+'); '+
      'map.setCenter(point, '+IntToStr(Scale)+')';
  end else begin
  // если не указан, то масштаб сохраняется
    Script:='var point = new GLatLng('+FloatToStr(Latitude)+','+FloatToStr(Longitude)+'); '+
      'map.setCenter(point)';
  end;
  ExecScript(Script);
end;

Добавление точки:

// формирование скрипта на добавление точки
function TGoogleMapFrame.PointToAddString(MapPoint:TMapPoint):String;
var
  ColorStr:String;
begin
  ColorStr:=ColorToString(MapPoint.Color)+'label';
  if (Copy(ColorStr,1,2)='cl') then
    Delete(ColorStr,1,2);
  Result:='createTLabel("%s", new GLatLng(%.8f, %.8f), "%s", "%s");';
  Result:=Format(Result,[MapPoint.Id,MapPoint.Latitude,MapPoint.Longitude,MapPoint.Caption,ColorStr]);
end;

// нарисовать точку с заданным индексом
procedure TGoogleMapFrame.ShowPoint(Index:Integer);
var
  Script:String;
begin
  // предварительно убить
  HidePoint(Index);
  // получаем скрипт
  Script:=PointToAddString(Points[Index]);
  // выполним
  ExecScript(Script);
end;

// спрятать точку с заданным индексом
procedure TGoogleMapFrame.HidePoint(Index:Integer);
var
  Script:String;
begin
  if Index>=PointsCount then
    Exit;
  // формируем скрипт
  Script:='map.removeTLabelById("'+Points[Index].Id+'");';
  ExecScript(Script);
end;

Функция PointToAddString возвращает скрипт по созданию точки. ShowPoint размещает точку из коллекции на карте. Вызов ShowPoint производится из методов базового класса. HidePoint убирает точку с карты.

Вызовы любых скриптов приводят к исключительной ситуации, если страница находится в процессе загрузки. Для обхода этих грабель я ввел событие загрузки страницы:

// отлавливаем событие загрузки карты
procedure TGoogleMapFrame.WebBrowserStatusTextChange(Sender: TObject;
  const Text: WideString);
begin
  if not FInitialized and (WebBrowser.ReadyState=READYSTATE_COMPLETE) then begin
    FInitialized:=True;
    if Assigned(FOnMapLoaded) then
      FOnMapLoaded(Self);
  end;
end;

При первом получении статуса браузера READYSTATE_COMPLETE вызывается событие OnMapLoaded. До срабатывания этого события работать со скриптами нельзя!

В классе TBaseMapFrame предусмотрены методы поддержки длительных изменений BeginUpate и EndUpdate. Смысл я думаю понятен. При построении больших маршрутов очень даже полезно.

В примерах лежат 2 приложения.

Приложение GoogleInteractiveMap демонстрирует большинство функционала фреймов. Для корректной работы необходимо в приложении отрыть файл "HTML\map-final.html". Только в нем есть все требуемые функции JavaScript. После загрузки страницы карты становятся доступными все функции программы. Маршруты подгружаются из каталога "Routes". Маршруты я создавал через Google Earth, т.к. он выдает в маршрутах некие осмысленные данные (но осознанно не строит маршрутов по России). В файле "RouteFromHelsinkiToRussia.txt" дорога из Хельсинки до Российской границы, более 1100 точек. На моей не самой медленной машинке с четырьмя ядрами загрузка занимает 1 минуту ровно. В файле "SmallRoute.txt" первые 10 точек маршрута.


Рисунок 1

Приложение RoutePlayer реализует работу с одной точкой и демонстрирует работу события OnMapLoaded. После загрузки карты начинается перемещение по точкам маршрута с периодом 1 секунда. Путь к файлу маршрута зашит в исходном тексте.


Рисунок 2

Видите синий прямоугольник со звездочкой в центре карты?

Все коды обильно закомментированы.

Как я говорил в начале, все проблемы обойти не удалось. Осталось у меня два вопроса, если кто-то подскажет мне решение, моя благодарность не будет знать границ в определенных объемах :):

  1. При достаточно большом маршруте, закрывая GoogleInteractiveMap, я получаю сообщение от IE о том, что скрипт тормозит работу и предложение прекратить выполнять скрипты. Я пытаюсь убивать все точки перед закрытием. Но, то ли я напутал что-то в событиях, то ли в силу природной тупости проблема не решается.
  2. Очень хотелось бы получать координаты под курсором. Насколько я понимаю, функционал у класса GMap2 для этого есть. Но как получить содержимое переменной JavaScript в Delphi я не догнал. Я понимаю, что путь лежит через DOM модель IE. Но как?

На этом, пожалуй, цикл гуглокартографии можно считать закрытым. Ну и отлично.

В эту обертку можно завернуть картографию Яндекс (лучше покрытие России), Yahoo, Microsoft (возможно). И особенно рекомендую посмотреть http://www.openstreetmap.org/, потому что OpenSource.

К статье прилагаются примеры