Как создать и удалить OpenLayers Feature Tooltip

Недавно пришлось реализовывать всплывающие подсказки к векторным объектам на карте OpenLayers. Сделал, как описано на mail-archive.com, но после первого появления tooltip при перемещении по карте появилась ошибка в объекте OpenLayers.Popup метод hide() «‘this.div.style’ — есть null или не является объектом» или «this.div is null».

Вот изначальный код
[code language=”javascript”]
var map = …, tooltipPopup = null;

// init …

map.addLayer(markers);
// create select for tooltips (OpenLayers Feature Tooltip)
var highlightCtrl = new OpenLayers.Control.SelectFeature(markers, {
hover: true,
highlightOnly: true,
renderIntent: ‘temporary’,
    eventListeners: {
  featurehighlighted: tooltipSelect,
  featureunhighlighted: tooltipUnselect
}
});
// add control to the map
map.addControl(highlightCtrl);
// activate tooltip control
highlightCtrl.activate();

function tooltipSelect(args) {
    if (typeof args.feature.tooltip != ‘undefined’ && args.feature.tooltip != null) {
        return;
    }
    // remove tooltip if exists
    if (tooltipPopup != null) {
        map.removePopup(tooltipPopup);
        tooltipPopup.destroy();
        tooltipPopup = null;
    }
    var htmlContent = ‘<span style="font-weight:bold">’ + args.feature.attributes.name + ‘</span><hr/>’;
    var center = args.feature.geometry.getBounds().getCenterLonLat();
    tooltipPopup = new OpenLayers.Popup(‘activetooltip’,
            center,
            new OpenLayers.Size(240, 25),
            htmlContent,
            false);
    tooltipPopup.closeOnMove = true;
    tooltipPopup.autoSize = true;
    // set tooltip style
    tooltipPopup.backgroundColor = ‘#000’;
    tooltipPopup.opacity = 0.85;
// jQuery wrapper
    $(tooltipPopup.div).css({
        ‘border-width’: ‘1px’,
        ‘border-color’: ‘#000’,
        ‘border-radius’: ‘4px’,
        ‘border-style’: ‘solid’,
        ‘padding’: ‘1px’,
        ‘margin-left’: ’10px’,
        ‘margin-top’: ‘4px’
    });
// jQuery wrapper
    $(tooltipPopup.contentDiv).css({
        ‘overflow’: ‘hidden’,
        ‘padding’: ‘8px’,
        ‘color’: ‘#fff’
    });
    args.feature.tooltip = tooltipPopup;
    map.addPopup(args.feature.tooltip);
}
function tooltipUnselect(args) {
    if (typeof args.feature.tooltip !== ‘undefined’ && args.feature.tooltip !== null) {
        map.removePopup(args.feature.tooltip);
        args.feature.tooltip.destroy();
        args.feature.tooltip = null;
        tooltipPopup = null;
    }
}
[/code]

Ошибку пришлось искать в исходниках OpenLayers и примерах для OpenLayers.Popup. В OpenLayers 2.11 ошибка по прежнему имеет место: все примеры, которые мне доводилось видеть хранят уже созданные окна popup в ассоциативных массивах и при повторном клике по ключу достают его оттуда и просто меняют контент (что-то вроде словаря или шаблона Object Pool, когда создание объекта — сложная операция и поэтому ненужные объекты «очищают» и складывают в «рюкзак»). Возможно, это правильная идея — но я решил сэкономить немного памяти и удалять popup полностью.

Ошибка кроется в последовательном вызове методов map.removePopup() и popup.destroy().
Если popup создается с параметром closeOnMove = true, то происходит подписка на событие this.map.events.register(«movestart» …).
Когда мы вызываем map.removePopup(), то карта удаляет из «себя» всплывающее окно, а заодно и чистит поле popup.map = null.
После этого tooltip.destroy() обнуляет все поля окна и пытается отписаться от события карты, но свойство карты уже сброшено в null popup.map, поэтому отписки не происходит и, не смотря на все наши старания, объект popup не удаляется из памяти и по-прежнему реагирует на событие карты «pan», вызывая метод popup.hide().

Менять исходник OpenLayers не стоит, а вот выйти из положения можно просто инициализировав поле popup.map снова.
Вот работающий код.
[code language=”javascript”]
var map = …, tooltipPopup = null;

// init …

map.addLayer(markers);
// create select for tooltips (OpenLayers Feature Tooltip)
var highlightCtrl = new OpenLayers.Control.SelectFeature(markers, {
hover: true,
highlightOnly: true,
renderIntent: ‘temporary’,
    eventListeners: {
  featurehighlighted: tooltipSelect,
  featureunhighlighted: tooltipUnselect
}
});
// add control to the map
map.addControl(highlightCtrl);
// activate tooltip control
highlightCtrl.activate();

function tooltipSelect(args) {
    if (typeof args.feature.tooltip != ‘undefined’ && args.feature.tooltip != null) {
        return;
    }
    // remove tooltip if exists
    if (tooltipPopup != null) {
        map.removePopup(tooltipPopup);
        tooltipPopup.destroy();
        tooltipPopup = null;
    }
    var htmlContent = ‘<span style="font-weight:bold">’ + args.feature.attributes.name + ‘</span><hr/>’;
    var center = args.feature.geometry.getBounds().getCenterLonLat();
    tooltipPopup = new OpenLayers.Popup(‘activetooltip’,
            center,
            new OpenLayers.Size(240, 25),
            htmlContent,
            false);
    tooltipPopup.closeOnMove = true;
    tooltipPopup.autoSize = true;
    // set tooltip style
    tooltipPopup.backgroundColor = ‘#000’;
    tooltipPopup.opacity = 0.85;
// jQuery wrapper
    $(tooltipPopup.div).css({
        ‘border-width’: ‘1px’,
        ‘border-color’: ‘#000’,
        ‘border-radius’: ‘4px’,
        ‘border-style’: ‘solid’,
        ‘padding’: ‘1px’,
        ‘margin-left’: ’10px’,
        ‘margin-top’: ‘4px’
    });
// jQuery wrapper
    $(tooltipPopup.contentDiv).css({
        ‘overflow’: ‘hidden’,
        ‘padding’: ‘8px’,
        ‘color’: ‘#fff’
    });
    args.feature.tooltip = tooltipPopup;
    map.addPopup(args.feature.tooltip);
}
function tooltipUnselect(args) {
    if (typeof args.feature.tooltip !== ‘undefined’ && args.feature.tooltip !== null) {
        map.removePopup(args.feature.tooltip);
        // fix: map property
        args.feature.tooltip.map = map;
        args.feature.tooltip.destroy();
        args.feature.tooltip = null;
        tooltipPopup = null;
    }
}
[/code]

Лицензия

Статья, а так же связанные с ней файлы и исходный код доступны по лицензии Apache License 2.0