Кластеризация меток на картах Google

На страницах своего блога, я рассказывал о том, как можно использовать кластеризацию на Яндекс.Картах («Кластеризация меток на Яндекс.Картах – два решения» ).

Как работает кластеризация?

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

В этой заметке я покажу как использовать тот же подход, для вывода большого числа маркеров с использованием API Google Maps v3.

Для реализации кластеризации на Google Maps существует специальная javascript-библиотека MarkerClusterer.

Покажу на нескольких примерах, как ей пользоваться.

В начале нам нужно подготовить исходные данные для примеров.

Это координаты расположения терминалов QIWI (555 – штук) в Нижнем Новгороде (использовались и в заметке«Кластеризация меток на Яндекс.Картах – два решения»).

Файл Qiwi-terminal.sql

Используя PhpMyAdmin, добавляем их в нашу базу данных MySQL.

Для вывода данных из базы и формирование файла в формате JSON, мы будем использовать файл vivmarkers.php

Его исходный код:

<?php
 
include("config.php");
 
header('Content-Type: text/html; charset=utf-8');
 
if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
 
$json = '{markers:['."n";
 
$query1= "SELECT * FROM qiwiterminal";
$result1 = mysql_query($query1);
 
if(mysql_num_rows($result1)>0)
{
while ($par1 = mysql_fetch_array($result1)){
 
$cname =  'Теминал QIWI';
 
$exp_str = explode(",", $par1['coords']);
 
$lat = $exp_str[0];
 
$lng = $exp_str[1];
 
$json.= "n".'{'.'"id": "'.$par1['id'].'",';
 
$json.= '"cname": "'.$cname.'",';
 
$json.= '"address": "'.$par1['address'].'",';
 
$json.= '"lat": "'.$lat.'",';
 
$json.=  '"lon": "'.$lng.'"';
 
$json.= '},';
 
}
 
$json = substr($json, 0,-1);
 
echo  $json;
 
echo '], ', "n",  '"status": "OK"', "n", '}';
 
}
else
{
echo '{"status": "false"}';
}
 
}
 
 
?>

Код файла config.php для подключения к базе данных:

<?php
 
$sdb_name = "localhost";
$user_name = "root";
$user_password = "";
$db_name = "gmap_bd";
 
// соединение с сервером базы данных
if(!$link = mysql_connect($sdb_name, $user_name, $user_password))
{
  echo "<br>Не могу соединиться с сервером базы данных<br>";
  exit();
}
 
// выбираем базу данных
if(!mysql_select_db($db_name, $link))
{
  echo "<br>Не могу выбрать базу данных<br>";
  exit();
}
 
mysql_query('SET NAMES utf8');
 
?>

Теперь когда исходные данные готовы рассмотрим реализацию примеров.

Первый пример кластеризации.

При загрузке карты мы видим, что маркеры сгруппированы в отдельные кластеры, с указанием числа входящих в них маркеров.

При каждом изменении масштаба, выполняется тот же алгоритм кластеризации.

С определенного уровня, мы наблюдаем отдельные маркеры.

Посмотреть пример в действии

Код файла my_primer_1.html:

<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>MarkerClusterer v3 пример использования</title>
 
    <style type="text/css">
      body {
        margin: 0;
        padding: 10px 20px 20px;
        font-family: Arial;
        font-size: 16px;
      }
 
      #map-container {
        padding: 6px;
        border-width: 1px;
        border-style: solid;
        border-color: #ccc #ccc #999 #ccc;
        -webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        -moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
        width: 800px;
      }
 
      #map {
        width: 800px;
        height: 600px;
      }
 
    </style>
 
    <script src="http://maps.google.com/maps/api/js?sensor=false"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
	<script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/src/markerclusterer.js"></script>
 
 
	<script type="text/javascript">
      function initialize() {
        var center = new google.maps.LatLng(56.316667, 44.0);
 
        var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 11,
          center: center,
          mapTypeId: google.maps.MapTypeId.ROADMAP
        });
 
        var markers = [];
           $.getJSON("vivmarkers.php", function (json) {
        if (json.status == 'OK') {
            var markers = [];
 
            for (var i = 0; i < json.markers.length; i++) {
 
          var latLng = new google.maps.LatLng(json.markers[i].lon,
              json.markers[i].lat);
          var marker = new google.maps.Marker({
            position: latLng,
			title: json.markers[i].address
          });
          markers.push(marker);
        }
        var markerCluster = new MarkerClusterer(map, markers);
		 } else {
            alert('Произошла ошибка!');
        }
    });
 
      }
      google.maps.event.addDomListener(window, 'load', initialize);
    </script>
  </head>
  <body>
    <h3>MarkerClusterer v3 пример использования</h3>
    <div id="map-container"><div id="map"></div></div>
  </body>
</html>

Рассмотрим код файла подробно.

В начале мы задаем стили для отображения окна с картой, затем подключаем необходимые javascript-библиотеки: API Google Maps v3, jQuery и MarkerClusterer.

В функции initialize мы задаем начальные параметры нашей карты: координаты центра, уровень масштаба и тип карты.

После этого используя функцию getJSON, мы загружаем исходные данные о маркерах, передаваемые из файла vivmarkers.php в массив markers.

После этого, кластеризатору через метод new MarkerClusterer, мы передаем указательна карту и массив меток.

И он после обработки, выводит их на карту.

Во втором примере, при клике по маркеру, мы будем выводить балун с информацией.

Посмотреть пример в действии

Код файла my_primer_2.html:

<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>MarkerClusterer v3 пример использования - информация в балуне</title>
 
    <style type="text/css">
      body {
        margin: 0;
        padding: 10px 20px 20px;
        font-family: Arial;
        font-size: 16px;
      }
 
      #map-container {
        padding: 6px;
        border-width: 1px;
        border-style: solid;
        border-color: #ccc #ccc #999 #ccc;
        -webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        -moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
        width: 800px;
      }
 
      #map {
        width: 800px;
        height: 600px;
      }
 
    </style>
 
    <script src="http://maps.google.com/maps/api/js?sensor=false"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
	<script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/src/markerclusterer.js"></script>
 
	<script type="text/javascript">
	var map;
 
function initialize() {
    var center = new google.maps.LatLng(56.316667, 44.0);
 
    map = new google.maps.Map(document.getElementById('map'), {
        zoom: 11,
        center: center,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    });
 
    var infowindow = new google.maps.InfoWindow({
        content: ''
    });
 
    $.getJSON("vivmarkers.php", function (json) {
        if (json.status == 'OK') {
            var markers = [];
 
            for (var i = 0; i < json.markers.length; i++) {
 
                var baloon = '<div><strong>' + json.markers[i].cname + '</strong><br>' + json.markers[i].address + '</div>'
 
                var marker = add_marker(json.markers[i].lon, json.markers[i].lat, json.markers[i].cname, baloon);
 
 
                markers.push(marker);
            }
            var markerCluster = new MarkerClusterer(map, markers);
        } else {
            alert('Произошла ошибка!');
        }
    });
}
 
function add_marker(lat, lng, title, box_html) {
 
    var infowindow = new google.maps.InfoWindow({
        content: box_html
    });
 
    var marker = new google.maps.Marker({
        position: new google.maps.LatLng(lat, lng),
        map: map,
        title: title
    });
 
    google.maps.event.addListener(marker, 'click', function () {
        infowindow.open(map, marker);
    });
 
    return marker;
}
 
$(document).ready(
function () {
    initialize();
    return true;
});
 
    </script>
  </head>
  <body>
    <h3>Пример использования кластеризации - информация в балуне</h3>
    <div id="map-container"><div id="map"></div></div>
  </body>
</html>

Отличие от первого примера — это дополнительная фуккция add_marker, которая формирует маркер и добавляет содержимое в его балун, отображаемое при клике на маркере.

Также, мы вынесли объявление объекта карты map в глобальные переменные.

У библиотеки MarkerClusterer есть дополнительные параметры, которые позволяют управлять порядком кластеризации.

Основные из них: gridSize — размер ячейки сетки в пикселях, maxZoom — максимальный масштаб для кластеризации, styles — позволяет задавать стиль отображения кластеров.

Подробнее

Третий пример, задание своего стиля для отображения кластеров.

Посмотреть пример в действии

Рассмотрим код файла my_primer_3.html:

<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>MarkerClusterer v3 пример использования - свой стиль маркеров</title>
 
    <style type="text/css">
      body {
        margin: 0;
        padding: 10px 20px 20px;
        font-family: Arial;
        font-size: 16px;
      }
 
      #map-container {
        padding: 6px;
        border-width: 1px;
        border-style: solid;
        border-color: #ccc #ccc #999 #ccc;
        -webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        -moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
        width: 800px;
      }
 
      #map {
        width: 800px;
        height: 600px;
      }
 
    </style>
 
    <script src="http://maps.google.com/maps/api/js?sensor=false"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
	<script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/src/markerclusterer.js"></script>
 
	<script type="text/javascript">
	var map;
 
	 var styles = [ [{
        url: '../images/znak30.png',
        height: 35,
        width: 30,
        anchor: [4, 0],
        textColor: '#f0ff3d',
        textSize: 10
      }, {
        url: '../images/znak40.png',
        height: 46,
        width: 40,
        anchor: [8, 0],
        textColor: '#ff0000',
        textSize: 11
      }, {
        url: '../images/znak50.png',
        width: 50,
        height: 58,
        anchor: [12, 0],
        textSize: 12
      }]];
 
	  var imageUrl = '../images/znak.png';
 
 
function initialize() {
    var center = new google.maps.LatLng(56.316667, 44.0);
 
    map = new google.maps.Map(document.getElementById('map'), {
        zoom: 11,
        center: center,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    });
 
    var infowindow = new google.maps.InfoWindow({
        content: ''
    });
 
	    $.getJSON("vivmarkers.php", function (json) {
        if (json.status == 'OK') {
 
            var markers = [];
 
			var markerImage = new google.maps.MarkerImage(imageUrl, new google.maps.Size(24, 28));
 
            for (var i = 0; i < json.markers.length; i++) {
 
                var baloon = '<div><strong>' + json.markers[i].cname + '</strong><br>' + json.markers[i].address + '</div>'
 
                var marker = add_marker(json.markers[i].lon, json.markers[i].lat, json.markers[i].cname, baloon, markerImage);
 
 
                markers.push(marker);
            }
 
var mcOptions = {gridSize: 50, maxZoom: 15, styles: styles[0]};
 
        markerClusterer = new MarkerClusterer(map, markers, mcOptions);
        } else {
            alert('Произошла ошибка!');
        }
    });
}
 
function add_marker(lat, lng, title, box_html, markerImage) {
 
    var infowindow = new google.maps.InfoWindow({
        content: box_html
    });
 
    var marker = new google.maps.Marker({
        position: new google.maps.LatLng(lat, lng),
        map: map,
        title: title,
		icon: markerImage
    });
 
    google.maps.event.addListener(marker, 'click', function () {
        infowindow.open(map, marker);
    });
 
    return marker;
}
 
$(document).ready(
function () {
    initialize();
    return true;
});
 
    </script>
  </head>
  <body>
    <h3>Пример использования кластеризации - свой стиль маркеров</h3>
    <div id="map-container"><div id="map"></div></div>
  </body>
</html>

В самом начале, мы определяем слиль для отображения кластеров

 var styles = [ [{
        url: '../images/znak30.png',
        height: 35,
        width: 30,
        anchor: [4, 0],
        textColor: '#f0ff3d',
        textSize: 10
      }, {
        url: '../images/znak40.png',
        height: 46,
        width: 40,
        anchor: [8, 0],
        textColor: '#ff0000',
        textSize: 11
      }, {
        url: '../images/znak50.png',
        width: 50,
        height: 58,
        anchor: [12, 0],
        textSize: 12
      }]];

Для разных значений размера ячеки сетки: 50, 40 и 30.

Иконку для отображения маркеров задает строка

var imageUrl = ‘../images/znak.png’;

Параметры для значка маркера строка

var markerImage = new google.maps.MarkerImage(imageUrl, new google.maps.Size(24, 28));

При вызове кластеризатора, мы передаем дополнительный параметр с опциями mcOptions:

var mcOptions = {gridSize: 50, maxZoom: 15, styles: styles[0]};

И в качестве дополнения, я покажу еще один пример использования кластеризатора, когда исходные данные о маркерах передаются в файле формата XML.

Посмотреть пример в действии

Созданием XML-файла с данными занимается скрипт vivmarkers_xml.php

Его код:

<?php
 
include("config.php");
 
header('Content-Type: text/xml; charset=utf-8');
 
if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
 
$dom = new DOMDocument("1.0");
$node = $dom->createElement("markers");
$parnode = $dom->appendChild($node); 
 
// Select all the rows in the markers table
$query = "SELECT * FROM qiwiterminal";
$result = mysql_query($query);
if (!$result) {  
  die('Invalid query: ' . mysql_error());
} 
 
// Iterate through the rows, adding XML nodes for each
 
while ($row = @mysql_fetch_assoc($result)){  
$cname =  'Теминал QIWI';
$exp_str = explode(",", $row['coords']);
$lat = $exp_str[0];
$lng = $exp_str[1];
 
  // ADD TO XML DOCUMENT NODE  
  $node = $dom->createElement("marker");  
  $newnode = $parnode->appendChild($node);   
  $newnode->setAttribute("cname",$cname);
  $newnode->setAttribute("address", $row['address']);  
  $newnode->setAttribute("lat", $lat);  
  $newnode->setAttribute("lon", $lng); 
} 
 
echo $dom->saveXML();
 
}
 
?>

Для формирования XML-файла я использую DOM-функции PHP версии 5.

В PHP нужно инициализировать новый XML-документ и создать родительский узел “markers”.

Затем нужно подключиться к БД и произвести выбор необходимых данных при помощи запроса вида
SELECT * (выбрать все) к таблице с именем “qiwiterminal” и пройтись по всем результатам этой выборки.

Для каждой записи в таблице будет создаваться свой узел в XML-документе, атрибуты которого будут полями из соответствующей записи таблицы, — этот узел будет присоединяться к родительскому узлу. После этого Вы получите готовый XML-документ.

Код файла my_primer_4.html:

<!DOCTYPE>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>MarkerClusterer v3 пример использования - данные в XML</title>
 
    <style type="text/css">
      body {
        margin: 0;
        padding: 10px 20px 20px;
        font-family: Arial;
        font-size: 16px;
      }
 
      #map-container {
        padding: 6px;
        border-width: 1px;
        border-style: solid;
        border-color: #ccc #ccc #999 #ccc;
        -webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        -moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
        box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
        width: 800px;
      }
 
      #map {
        width: 800px;
        height: 600px;
      }
 
    </style>
 
    <script src="http://maps.google.com/maps/api/js?sensor=false"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
	<script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/src/markerclusterer.js"></script>
 
	<script type="text/javascript">
      function initialize() {
        var center = new google.maps.LatLng(56.316667, 44.0);
 
        var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 11,
          center: center,
          mapTypeId: google.maps.MapTypeId.ROADMAP
        });
 
        var markers = [];
 
	$.ajax({
    type: "GET",
    url: "vivmarkers_xml.php",
    dataType: "xml",
    success: function(xml) {
			$(xml).find("marker").each(function(){
	      	var lat = $(this).attr('lon');
		    var lng = $(this).attr('lat');				
		    var latLng = new google.maps.LatLng(parseFloat(lat), parseFloat(lng));
 
			var marker = new google.maps.Marker({
				position: latLng,
				title: $(this).attr('address')
			});
          markers.push(marker);
		           });
 
 
        var markerCluster = new MarkerClusterer(map, markers);
		}
		 });
 
      }
      google.maps.event.addDomListener(window, 'load', initialize);
    </script>
  </head>
  <body>
    <h3>Пример использования кластеризации - данные в XML</h3>
    <div id="map-container"><div id="map"></div></div>
  </body>
</html>

Здесь отличие от первого примера в AJAX запросе данных в формате XML, а также в обработке полученных данных.

Еще несколько ссылок на примеры использования MarkerClusterer идущие вместе с ним:

A simple example of MarkerClusterer (100 markers);
An example of MarkerClusterer v3;
An example of MarkerClusterer v3 speed test

Загрузить саму библиотеку со всеми примерами можно здесь.

  • Гость: Я бы еще порекомендовал добавить в описание MarkerClustererPlus, текущяа версия http://google-maps-utility-library-v3.googlecode.com/svn/tags/markerclustererplus/2.0.7/docs/reference.html Т.к. он и быстрее, и у него есть то, чего нет у обычного кластерера - вынос из кластера массива меток removeMarkers(markers:Array., opt_nodraw?:boolean) Иначе если Вы будете удалять по одной метке - получите сильную задержку, я так понимаю из-за отрисовки после изменения содержимого кластера, т.е. в случае удаления целого массива, перерисовка производится после удаления, а не после каждого маркера :) Это только догадки, хотя можно посмотреть код
  • Гость: Отличная штука! Я так понимаю - если я хочу, чтобы в кластерах отображались объекты недвижимости, то данные будут браться из базы данных недвижимости? Каким образом будут ставиться метки? Может есть где почитать - где кроме Панорамио можно ставить метки на карте? Спасибо!
  • Гость: Я имел в виду пример speed_test
  • Гость: отличная статья, благодаря ей нанес на карту членов ассоциации с кластеризацией! спасибо автору!
  • Гость: Не встречались ли с такой задачей: Есть скажем 2 группы меток (отображаются ввиде списка рядом с картой): по несколько меток в группе А и несколько в группе Б. При нажатии на группу А отображаются все метки этой группы, а если выбрать одну из меток в группе А, то отобажается только она. Тоже самое с Группой Б. Есть решение?
  • Гость: Спасибо громаадное за статью!! Очень помогла! Благодарю автора!!!
  • Гость: Статья - как и другие - на высоте, спасибо автору У меня возник такой вопрос: в примере 2 (среди отметок - справа, клик по числу 38, потом при увеличении к отметкам примерно по адресу Казанское шоссе, дом 10х3) есть кластер из 3 отметок, но при самом большом зуме он так на отдельные отметки не расспадается. Думаю, это из-за того, что все 3 - находяться на одном адресе... Вот как их показать отдельно? отключить кластеризацию?
  • Гость: у меня это (пока в разработке, не пинайте) http://mapeteua/infrastruktura выбрать "Місцеве самоврядування" примерно по центру - кластер с числом 24, клик улица "Листопадова" (слева) кластер с числом 12, клик по адресу Листопадова, 5 (это горсовет) получается как бы одинарная иконка (или 12), вот как можна вывести их отдельно?
  • Гость: Спасибо Андрею за вопрос, тоже стало интересно, как разделить метки, которые находятся по одному адресу?..
  • Гость: Подскажите пожалуйста, никак не могу подружить данные скрипты с версией query 1.7 Уже совсем измучалась
  • Гость: Чтобы разделить метки, которые находятся по одному адресу используйте ограничение по максимальному зуму, при котором срабатывает кластеризация: var markerCluster = new MarkerClusterer(map, markers_cluster, {maxZoom: 14, .... }
  • Гость: А можно ли в сделать не отдельные, а один общий кластер, где будут показывать два числа разных маркеров? Т.е, например, в одном кластере писать "12" синих и "8" желтых маркеров? Спасибо! . С ув, Анатолий