Сегодня трудно представить себе жизнь без карт. Они повсюду: в смартфонах, GPS-навигаторах, в прогнозах погоды. Закономерно, что карты нашли свое место и в интернете.

Часто на сайтах возникает необходимость отобразить карту региона, страны или мира в контексте тематики сайта. Примером может быть информация о региональных представительствах или продуктовом ассортименте, представленном в том или ином регионе. Иногда, в силу разных факторов, это требует создания индивидуальных решений. В этой статье я расскажу об одном из таких решений.

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

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

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

Для масштабирования карты мы могли бы использовать CSS-свойство background-size со значением cover. Однако на сегодняшний день получить размеры фоновой картинки после масштабирования невозможно, а размеры нам понадобятся для сохранения позиций отметок на карте.

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

Для начала создадим HTML-страницу с кодом будущей карты:

<div class="map-wrapper">

    <div data-toggle="ps-pin-map" class="map">
        <img class="map-image" src="img/map.jpg">
    </div>

</div>

Карту мы помещаем на страницу с помощью тега img, оборачиваем его в контейнер .map, на котором будем инициализировать будущий скрипт. Все это оборачиваем в контейнер .map-wrapper, который определяет область, выделенную под карту. В моем примере эта область будет занимать весь экран.

Теперь создадим стили для нашего HTML-кода:

html,
body {
    position: relative;
    height: 100%;
}
body {
    min-height: 548px;
    min-width: 1024px;
}
.map-wrapper {
    width: 100%;
    height: 100%;
    position: absolute;
    overflow: hidden;
    z-index: 2;
}
.map {
    height: 100%;
    min-width: 1024px;
    min-height: 548px;
    position: absolute;
    left: 50%;
    top: 0;
    bottom: 0;
}
.map-image {
    display: block;
    height: 100%;
    top: 0;
    z-index: 1;
}

Масштабирование изображения происходит автоматически за счет свойства height: 100%; на элементе .map-image. У элемента .map есть минимально допустимые ширина и высота, чтобы карта не становилась слишком уж маленькой, пусть лучше будет полоса прокрутки, чем микроскопическая карта. А вот центрировать карту будет скрипт: элементу .map будет устанавливаться левый отступ с отрицательным значением.

Добавим на карту несколько отметок:

...
<img class="map-image" src="img/map.jpg">

<button class="map-pin" data-pos="510;134"></button>
<button class="map-pin" data-pos="686;502"></button>
<button class="map-pin" data-pos="768;165"></button>
<button class="map-pin" data-pos="906;232"></button>
...
.map-pin {
    background: url('../img/pin.png') no-repeat 0 0;
    margin: -11px 0 0 -11px;
    outline: 0;
    border: 0;
    padding: 0;
    display: block;
    width: 22px;
    height: 22px;
    position: absolute;
    top: -22px;
    left: -22px;
    z-index: 3;
}
.map-pin:hover {
    z-index: 5;
}

Каждой отметке в атрибут data-pos запишем координаты на карте. В качестве координат в нашем случае горизонтальное и вертикальное смещение центра отметки относительно левого верхнего угла изображения с картой. При масштабировании эти значения будут использованы для вычисления реальной позиции отметок.

Теперь переходим к написанию скрипта. Скрипт я сразу оформлю как плагин для jQuery в стиле библиотеки Bootstrap. Для начала напишем код центрирования карты:

(function ($) {
    "use strict";

    // Define plugin name once to use throughout the script
    var pluginName = 'psPinMap';

    // Define plugin defaults
    var pluginDefaults = {
        imageSelector: '.map-image'
    };

    // Constructor function
    function Plugin (element, options) {
        this.init(pluginName, element, options);
    }

    Plugin.prototype = {

        constructor: Plugin,

        init: function (name, element, options) {
            // Plugin name is passed for extendability
            this.name     = name;
            this.$element = $(element);
            this.options  = $.extend({}, $.fn[this.name].defaults, this.$element.data(), options);

            var self = this;

            // Attach resize handler
            $(window).on('resize.ps.placemark-scale', function () {
                self.resize();
            });
        },

        resize: function () {
            var $image = this.$element.find(this.options.imageSelector);

            this.$element.css('margin-left', '-' + Math.round($image.width() / 2) + 'px');
        }

    };

    // Plugin wrapper for outside use
    $.fn[pluginName] = function (option) {
        var args = Array.prototype.slice.call(arguments, 1);
        return this.each(function () {
            var $this   = $(this),
                data    = $this.data(pluginName),
                options = typeof option == 'object' && option;
            if (!data) $this.data(pluginName, (data = new Plugin(this, options)));
            if (typeof option == 'string') data[option].apply(data, args);
        });
    };

    // Expose constructor for extendability
    $.fn[pluginName].Constructor = Plugin;

    // Expose default options to make them changeable
    $.fn[pluginName].defaults = pluginDefaults;

    $('[ data-toggle="ps-pin-map"]')[pluginName]();

})(window.jQuery);

Каждый раз после изменения размеров окна браузера наш скрипт вызывает метод resize(), который заново центрирует карту на экране. Но нужно учесть, что не во всех браузерах это событие срабатывает сразу после загрузки страницы, да и самой карте нужно время чтоб полностью загрузиться. Поэтому немного допишем метод resize() и добавим вызов этого метода в конец метода init():

Plugin.prototype = {

    constructor: Plugin,

    init: function (name, element, options) {
        // Plugin name is passed for extendability
        this.name     = name;
        this.$element = $(element);
        this.options  = $.extend({}, $.fn[this.name].defaults, this.$element.data(), options);

        var self = this;

        // Attach resize handler
        $(window).on('resize.ps.placemark-scale', function () {
            self.resize();
        });

        this.resize();
    },

    resize: function () {
        var $image = this.$element.find(this.options.imageSelector),
            self   = this;

        this.$element.css('margin-left', '-' + Math.round($image.width() / 2) + 'px');

        var img;
        if (!this._image) {
            img = this._image = new Image();

            $(img).one('load', function () {
                self.resize();
            }).one('error', function () {
                // Image invalid, stop the plugin
                $(window).off('resize.ps.placemark-scale');
            });

            img.src = $image.attr('src');
            return;
        }
    }

};

Теперь скрипт вызывает метод resize() сразу после инициализации плагина. А метод resize(), в свою очередь, вызывает себя повторно после окончательной загрузки карты. Теперь можно добавить в плагин код, пересчитывающий позиции отметок:

Plugin.prototype = {

    // ...

    resize: function () {
        var $placemarks = this.$element.find(this.options.placemarkSelector),
            $image      = this.$element.find(this.options.imageSelector),
            self        = this;

        this.$element.css('margin-left', '-' + Math.round($image.width() / 2) + 'px');
        if ($placemarks.length < 1) return;

        var img;
        if (!this._image) {
            img = this._image = new Image();

            $(img).one('load', function () {
                self.resize();
            }).one('error', function () {
                // Image invalid, stop the plugin
                $(window).off('resize.ps.placemark-scale');
            });

            img.src = $image.attr('src');
            return;
        }

        img = this._image;
        var primary = this.options.primary,
            ratio   = this.$element[primary == 'y' ? 'height' : 'width']() / img['natural' + (primary == 'y' ? 'Height' : 'Width')];

        $placemarks.each(function () {
            var $this = $(this),
                pos   = $this.data('pos').split(';'),
                x     = Number(pos[0]),
                y     = Number(pos[1]),
                left  = Math.round(x * ratio),
                top   = Math.round(y * ratio);
            $this.css({
                left: left + 'px',
                top:  top + 'px'
            });
        });
    }

};

После центрирования карты метод resize() вычисляет координаты всех отметок на основе вычисляемого коэффициента масштабирования ratio и значений в атрибуте data-pos каждой из отметок. Осталось подключить наш плагин к странице, и полноэкранная карта готова.

 

Подход, изложенный в статье, подходит для презентационных сайтов, где количество и позиционирование отметок определено заранее и не подвержено существенным изменениям. Однако, что-либо более серьезное потребует от разработчика иного подхода. Вполне возможно создание решений на основе Google- и Яндекс-карт, на пару с возможностью устанавливать, перемещать и удалять точки на карте из админ-панели.

А «пощупать» описанное в статье решение можно здесь.