Клуб API Карт

Создание меню для отображения коллекций геообъектов

Пост в архиве.
Создание меню для отображения коллекций геообъектов

В последнее время в клубе нас часто спрашивают различные варианты примера меню для групп геообъектов. Так как все возможные кейсы предусмотреть невозможно, мы решили написать статью и по шагам объяснить подход к созданию такого меню.
Чтобы задать логику элементам меню, нужно связать его DOM-представление с ссылкой на объект API, с которым этот пункт меню будет взаимодействовать. Мы будем слушать событие 'click' на DOM-элементе и удалять или добавлять соответствующий геообъект с карты через API.


Построение меню из массива исходных данных

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

Допустим у нас есть такой массив исходных данных:



// Группы объектов
var groups = [{
name: "Известные памятники",
preset: "twirl#redIcon",
style: "important",
items: [{
center: [50.426472, 30.563022],
name: "Монумент 'Родина-Мать'"
}, {
center: [50.45351, 30.516489],
name: "Памятник Богдану Хмельницкому"
}, {
center: [50.454433, 30.529874],
name: "Арка Дружбы народов"
}]
}, {
name: "Покушайки",
preset: "twirl#greenIcon",
style: "success",
items: [{
center: [50.50955, 30.60791],
name: "Ресторан 'Калинка-Малинка'"
}, {
center: [50.429083, 30.521708],
name: "Бар 'Сало-бар'"
}, {
center: [50.450843, 30.498271],
name: "Абсент-бар 'Палата №6'"
}, {
center: [50.454834, 30.516498],
name: "Ресторан 'Спотыкач'"
}]
}, {
name: "Оригинальные музейчики",
preset: "twirl#orangeIcon",
style: "warning",
items: [{
center: [50.443334, 30.520163],
name: "Музей грамзаписи и старинных музыкальных инструментов"
}, {
center: [50.446977, 30.505269],
name: "Музей истории медицины или Анатомический театр"
}, {
center: [50.452512, 30.530889],
name: "Музей воды. Водно-информационный центр"
}]
}, {
name: "Красивости",
preset: "twirl#blueIcon",
style: "info",
items: [{
center: [50.45987, 30.516174],
name: "Замок Ричарда-Львиное сердце"
}, {
center: [50.445049, 30.528598],
name: "'Дом с химерами'"
}, {
center: [50.449156, 30.511809],
name: "Дом Рыцаря"
}]
}];


Чтобы создать на его основе меню и маркеры на карте, нам необходимо в цикле перебирать элементы этого массива, например, с помощью стандартного метода forEach:


// Контейнер для меню.
var menu = $('<ul class="nav nav-list"/>');
// Перебираем все группы.
groups.forEach(function (group) {
// DOM-представление группы.
var menuItem = $('<li class="nav-header">' + group.name + ''),
// Создадим коллекцию для геообъектов группы.
collection = new ymaps.GeoObjectCollection(null, { preset: group.preset });
});


Для каждого элемента массива, представляющего собой описание группы геообъектов, мы создаем DOM-представление в виде элемента меню и представление на карте: коллекцию. Затем мы добавляем коллекцию на карту, а элемент — в меню. Мы будем обрабатывать клики на элементе и при их возникновении будем удалять или добавлять соответствующую коллекцию на карту, а также скрывать или отображать подменю.


// Добавляем коллекцию на карту.
myMap.geoObjects.add(collection);
menuItem
// Добавляем пункт в меню.
.appendTo(menu)
// Навешиваем обработчик клика по пункту меню.
.on('click', function (e) {
// Скрываем/отображаем пункты меню данной группы.
$(this)
.nextUntil('.nav-header')
.removeClass('active')
.slideToggle('fast');
// Скрываем/отображаем коллекцию на карте.
if (collection.getParent()) {
myMap.geoObjects.remove(collection);
} else {
myMap.geoObjects.add(collection);
}
});


Далее мы обрабатываем все элементы группы также с помощью forEach:


// Перебираем элементы группы.
group.items.forEach(function (item) {
// DOM-представление элемента группы.
var menuItem = $('<li><a href="javascript:void(0)">' + item.name + '</a></li>'),
// Создаем метку.
placemark = new ymaps.Placemark(item.center, { balloonContent: item.name });
// Добавляем метку в коллекцию.
collection.add(placemark);
menuItem
// Добавляем пункт в меню.
.appendTo(menu)
// Навешиваем обработчик клика по пункту меню.
.on('click', function (e) {
// Отменяем основное поведение (переход по ссылке)
e.preventDefault();
// Выставляем/убираем класс active.
menuItem
.toggleClass('active')
.siblings('.active')
.removeClass('active');
// Открываем/закрываем баллун у метки.
if (placemark.balloon.isOpen()) {
placemark.balloon.close();
} else {
// Плавно меняем центр карты на координаты метки.
myMap.panTo(placemark.geometry.getCoordinates(), {
delay: 0,
callback: function () {
placemark.balloon.open();
}
});
}
});
});


Здесь по аналогии мы создаем API-представление для каждого элемента группы в виде метки, которую добавляем в API-представление группы (коллекцию), и DOM-представление в виде элемента меню, который добавляем в меню после пункта соответствующей группы. Затем навешиваем на него обработчик события 'click', при возникновении которого будем открывать или закрывать балун у метки, а также менять центр карты.

После обработки всех данных нам остается добавить меню на страницу и установить область видимости карты в соответствии с её содержимым:


// Добавляем меню в сайдбар.
menu.appendTo($('#sidebar'));
// Выставляем масштаб карты чтобы были видны все группы.
myMap.setBounds(myMap.geoObjects.getBounds());


Итоговый код можно посмотреть тут.

Построение меню на основе YMapsML-файла

Этот вариант является достаточно распространенным, т.к. довольно удобно хранить коллекции геообъектов в формате XML у себя на сервере или на maps.yandex.ru в Моих картах. Реализовать его, пожалуй, даже проще в связи с тем, что geoXML-загрузчик возвращает нам готовые API-коллекции геообъектов, которые нам остается только добавить на карту.


// Загрузка YMapsML-файла.
ymaps.geoXml.load("http://api.yandex.ru/maps/doc/ymapsml/1.x/examples/xml/menufromymapsml.xml")
.then(function (res) {
// Все содержимое YMapsML файла возвращается ввиде коллекции.
var groups = res.geoObjects,
// Стили для DOM-представлений групп.
styles = {
"default#redPoint": "important",
"default#greenPoint": "success",
"default#orangePoint": "warning",
"undefined": "info"
};
// Добавляем все группы на карту.
myMap.geoObjects.add(groups);
});


Для построения меню нам нужно перебирать эти коллекции, например, с помощью метода each или итератора. Затем, получив в обработчике элементы коллекции, строить по ним пункты меню, связывая их с геообъектами посредством интерфейса DOM-событий.
Данные для пунктов меню (название) в этом варианте мы получаем из данных геообъектов и коллекций.


// Перебираем все группы.
groups.each(function (group) {
// Определяем стиль для группы.
var preset = group.options.get('preset') || 'twirl#blueIcon',
style = styles[preset.match(/#([a-z]+)[A-Z]/)[1]],
// Имя группы.
name = group.properties.get('name'),
// DOM-представление группы.
menuItem = $('<li class="nav-header">' + name + '');
menuItem
// Добавляем пункт в меню.
.appendTo(menu)
// Навешиваем обработчик клика по пункту меню.
.on('click', function (e) {
// Скрываем/отображаем пункты меню данной группы.
$(this)
.nextUntil('.nav-header')
.removeClass('active')
.slideToggle('fast');
// Скрываем/отображаем коллекцию на карте.
if (group.getParent()) {
groups.remove(group);
} else {
groups.add(group);
}
});
});


С элементами группы та же история, geoXML загрузчик превратит их в готовые метки, и нам не придется их самим создавать и добавлять на карту. Нужно лишь позаботиться об их DOM-представлении.


// Перебираем элементы группы.
group.each(function (item) {
var name = item.properties.get('name'),
// DOM-представление элемента группы.
menuItem = $('<li><a href="javascript:void(0)">' + name + '</a></li>');
// Добавляем пункт в меню.
menuItem
.appendTo(menu)
// Навешиваем обработчик клика по пункту меню.
.on('click', function (e) {
e.preventDefault();
// Выставляем/убираем класс active.
menuItem
.toggleClass('active')
.siblings('.active')
.removeClass('active');
// Открываем/закрываем балун у метки.
if (item.balloon.isOpen()) {
item.balloon.close();
} else {
// Плавно меняем центр карты на координаты метки.
myMap.panTo(item.geometry.getCoordinates(), {
delay: 0,
callback: function () {
item.balloon.open();
}
});
}
});
});


Итоговый пример можно посмотреть тут.

Подключение существующего меню к геообъектам на карте

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

Это можно сделать, проставив DOM-элементам атрибуты id, а геообъектам передать произвольное поле в объекте-данных (поле properties в первом параметре в конструкторе коллекции и второй параметр в конструкторе метки).

Таким образом создание коллекции с названием и идентификатором будет выглядеть следующим образом:


var group1 = new ymaps.GeoObjectArray({
properties: {
id: 'group-1',
name: 'Известные памятники'
}
}, {
preset: 'twirl#redIcon'
});


Далее наполним коллекцию метками с помощью метода add:


group1
.add(new ymaps.Placemark([50.426472, 30.563022], { id: 'group-1-1', balloonContent: 'Монумент "Родина-Мать"' }))
.add(new ymaps.Placemark([50.45351, 30.516489], { id: 'group-1-2', balloonContent: 'Памятник "Богдану Хмельницкому"' }))
.add(new ymaps.Placemark([50.454433, 30.529874], { id: 'group-1-3', balloonContent: 'Арка Дружбы народов' }));


Поскольку в данном примере мы не будем хранить ссылки на коллекции, мы поместим наши коллекции в еще одну коллекцию, назовем ее visible и добавим ее на карту. Чтобы скрыть коллекции с карты мы будем переносить их (добавлять) в другую коллекцию, назовем ее hidden, которую мы не будем добавлять на карту. Стоит отметить в API все геообъекты и коллекции реализуют интерфейс IChildOnMap. Следовательно добавление геообъекта или коллекции в другую коллекцию удалит его из текущей коллекции. В нашем случае перенос коллекции геообъектов из visible в hidden приведет к удалению этой коллекции с карты.


// Добавляем все группы в одну коллекцию.
visible
.add(group1)
.add(group2)
.add(group3)
.add(group4);
// Добавляем все группы на карту.
myMap.geoObjects.add(visible);


Теперь если мы добавим коллекцию group1 в другую коллекцию hidden,


hidden.add(group1);


она автоматически будет удалена из коллекции visible и соответственно с карты.

Поместим код управления видимостью коллекций на карте в отдельный метод groupToggle, принимающий идентификатор коллекции.


// Ищем нужную группу и добавляем/удаляем ее с карты.
function groupToggle (id) {
// Все группы лежат внутри одной коллекции, которую мы получили из YMapsML.
var it, group;
// Сначала ищем в видимой на карте коллекции.
it = visible.getIterator();
while (group = it.getNext()) {
if (group.properties.get('id') === id) {
hidden.add(group);
return;
}
}
// Если мы сюда попали, значит коллекция уже удалена и надо искать в удаленных.
it = hidden.getIterator();
while (group = it.getNext()) {
if (group.properties.get('id') === id) {
visible.add(group);
return;
}
}
}


В данном методе мы будем использовать итератор вместо уже знакомого нам метода each. Он имеет преимущество, так как мы можем при нахождении нужного элемента завершить перебор коллекции и выйти из функции вызовом return.

Обработчик клика на DOM-представлении коллекций будет выглядеть следующим образом:


$('.nav-header').on('click', function (e) {
// Скрываем/отображаем пункты меню данной группы.
$(this)
.nextUntil('.nav-header')
.removeClass('active')
.slideToggle('fast');
// Скрываем/отображаем коллекцию на карте.
groupToggle(this.id);
});


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


// Обрабатываем клики на пунктах меню элементов группы.
$('li:not(.nav-header)').on('click', function (e) {
// Отменяем основное поведение (переход по ссылке)
e.preventDefault();
$(this)
.toggleClass('active')
.siblings('.active')
.removeClass('active');
itemToggle(this.id);
});


В методе itemToggle также будем использовать итераторы:


// Ищем нужную метку и открываем/закрываем ее балун.
function itemToggle (id) {
var it = visible.getIterator(),
group;
while (group = it.getNext()) {
for (var i = 0, len = group.getLength(); i < len; i++) {
var placemark = group.get(i);
if (placemark.properties.get('id') === id) {
if (placemark.balloon.isOpen()) {
placemark.balloon.close();
} else {
myMap.panTo(placemark.geometry.getCoordinates(), {
delay: 0,
callback: function () {
placemark.balloon.open();
}
});
}
return;
}
}
}
}


Итоговый пример можно посмотреть тут.