Выделение произвольной области на Яндекс картах

Обзор обновления aveCRM 4.2
Как верстать email письма. Часть 1
Выделение произвольной области на Яндекс картах

Задача: добавить инструментарий на Яндекс карты для выделения произвольной области.

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

Рассмотрим на примере добавление дополнительной панели инструментов на карту.

html разметка блока карты

<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>
<div class="map_container">
    <div class="mapMenu">
        <div class="title">Выделение области:</div>
        
        <button class="mapMenuIcon hidden">
            <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 25 25">
                <g fill="none" fill-rule="evenodd" stroke="currentColor">
                    <path stroke-width="2" d="M17 2l5 11-9 9-11-6L4 4z"></path>
                    <path fill="#FFF" d="M3.3 5.348c.716.414 1.634.168 2.048-.55.414-.716.168-1.634-.55-2.048-.716-.414-1.634-.168-2.048.55-.414.716-.168 1.634.55 2.048zm13-2c.716.414 1.634.168 2.048-.55.414-.716.168-1.634-.55-2.048-.716-.414-1.634-.168-2.048.55-.414.716-.168 1.634.55 2.048zm5 11c.716.414 1.634.168 2.048-.55.414-.716.168-1.634-.55-2.048-.716-.414-1.634-.168-2.048.55-.414.716-.168 1.634.55 2.048zm-9 9c.716.414 1.634.168 2.048-.55.414-.716.168-1.634-.55-2.048-.716-.414-1.634-.168-2.048.55-.414.716-.168 1.634.55 2.048zm-11-6c.716.414 1.634.168 2.048-.55.414-.716.168-1.634-.55-2.048-.716-.414-1.634-.168-2.048.55-.414.716-.168 1.634.55 2.048z"></path>
                </g>
            </svg>
        </button>
        
        <button id="draw_line" class="mapMenuIcon">
            <svg xmlns="http://www.w3.org/2000/svg" width="28" height="25" viewBox="0 0 28 25">
                <g fill="none" fill-rule="evenodd" stroke="currentColor" transform="rotate(90 12.5 14)">
                    <path stroke-width="2" d="M5.9 3.273c2.466-2.195 13.518-5.957 14.01-.302.156 1.79-1.874 3.436-1.475 5.187.393 1.723 3.61 2.214 3.565 3.98-.042 1.61-3.015 1.694-3.75 3.135-1.863 3.658 4.84 5.727 2.15 8.984-3.06 3.714-20.53-5.01-20.398-7.9.164-3.59 4.257-1.693 6.206-3.797 2.53-2.73-8.662-6.157-2.704-8.322L5.9 3.273z"></path>
                    <circle cx="8.5" cy="22.5" r="1.5" fill="#FFF"></circle>
                </g>
            </svg>
        </button>
        
        <button class="mapMenuIcon hidden">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
                <g fill="none" fill-rule="evenodd" stroke="currentColor" transform="translate(1 1)">
                    <path stroke-width="2" d="M11 22c6.075 0 11-4.925 11-11S17.075 0 11 0C9.179 0 7.46.443 5.948 1.226A11 11 0 0 0 0 11c0 6.075 4.925 11 11 11z"></path>
                    <circle cx="18.5" cy="18.5" r="1.5" fill="#FFF"></circle>
                </g>
            </svg>
        </button>
    </div>

    <div id="map" style="width: 100%; height: 800px;"></div>
    <canvas id="draw-canvas" style="position: absolute; left: 0; top: 0; display: none;"></canvas>
</div>
    

Стили

        .map_container {
            position:relative;
        }
        .mapMenu {
            position: absolute;
            z-index: 1;
            top: 20px;
            left: calc(50% - 130px);
            padding: 0 15px;
            display: -webkit-box;
            display: -webkit-flex;
            display: -ms-flexbox;
            display: flex;
            height: 46px;
            background-color: #fff;
            border-radius: 24px;
            -webkit-box-shadow: 0 2px 8px 0 rgba(0,0,0,.25);
            box-shadow: 0 2px 8px 0 rgba(0,0,0,.25);
            -webkit-box-pack: center;
            -webkit-justify-content: center;
            -ms-flex-pack: center;
            justify-content: center;
            -webkit-box-align: center;
            -webkit-align-items: center;
            -ms-flex-align: center;
            align-items: center;
        }

        .mapMenu .title {
            padding: 0 10px;
            font-size: 14px;
            line-height: 1.57;
            color: #505152;
        }

        .mapMenu .mapMenuIcon {
            padding: 5px;
            width: 40px;
            height: 35px;
            cursor: pointer;
            color: #7b7b7b;
            background: 0 0;
            border: 0;
            outline-style: none;
        }
        .mapMenu .mapMenuIcon:hover {
            color: #000
        }

        .mapMenu .mapMenuIcon.active, .mapMenu .mapMenuIcon.active:hover {
            color: #147de8
        }
    

Скрипт

        $(function () {

            var polygonOptions = {
                strokeColor: '#1679e7',
                fillColor: '#a2c9d8',
                interactivityModel: 'default#transparent',
                strokeWidth: 4,
                opacity: 0.7
            };
        
            var canvasOptions = {
                strokeStyle: '#1679e7',
                lineWidth: 4,
                opacity: 0.7
            };
        
        
            ymaps.ready(['Map', 'Polygon']).then(function () {
                var map = new ymaps.Map('map', {
                    center: [59.91807704072416, 30.30557799999997],
                    zoom: 12
                });
                map.behaviors.disable('scrollZoom');
                var polygon = null;
        
                if ($("input[name=in_poligon]").val() != '') {
                    var point = $("input[name=in_poligon]").val();
                    point = point.split(',');
        
                    var point_array = [];
                    point.forEach(element => {
                        el = element.split('_')
                        point_array.push([el[1], el[0]]);
                    });
        
                    // Создаем многоугольник, используя класс GeoObject.
                    var myGeoObject = new ymaps.GeoObject({
                        // Описываем геометрию геообъекта.
                        geometry: {
                            // Тип геометрии - "Многоугольник".
                            type: "Polygon",
                            // Указываем координаты вершин многоугольника.
                            coordinates: [point_array],
                            // Задаем правило заливки внутренних контуров по алгоритму "nonZero".
                            fillRule: "nonZero"
                        },
                        // Описываем свойства геообъекта.
                        properties: {
                            // Содержимое балуна.
                            balloonContent: "Многоугольник"
                        }
                    }, {
                        // Описываем опции геообъекта.
                        // Цвет заливки.
                        fillColor: '#a2c9d8',
                        // Цвет обводки.
                        strokeColor: '#1679e7',
                        // Общая прозрачность (как для заливки, так и для обводки).
                        opacity: 0.7,
                        // Ширина обводки.
                        strokeWidth: 4
                        // Стиль обводки.
                    });
        
                    // Добавляем многоугольник на карту.
                    map.geoObjects.add(myGeoObject);
                }
        
                var drawButton = document.querySelector('#draw_line');
        
                drawButton.onclick = function () {
                    drawButton.disabled = true;
        
                    drawLineOverMap(map)
                        .then(function (coordinates) {
                            // Переводим координаты из 0..1 в географические.
                            var bounds = map.getBounds();
                            coordinates = coordinates.map(function (x) {
                                return [
                                    // Широта (latitude).
                                    // Y переворачивается, т.к. на canvas'е он направлен вниз.
                                    bounds[0][0] + (1 - x[1]) * (bounds[1][0] - bounds[0][0]),
                                    // Долгота (longitude).
                                    bounds[0][1] + x[0] * (bounds[1][1] - bounds[0][1]),
                                ];
                            });
        
                            // Тут надо симплифицировать линию.
                            // Для простоты я оставляю только каждую третью координату.
                            coordinates = coordinates.filter(function (_, index) {
                                return index % 3 === 0;
                            });
        
                            // Удаляем старый полигон.
                            if (polygon) {
                                map.geoObjects.remove(polygon);
        
                            }
        
                            // Создаем новый полигон
                            polygon = new ymaps.Polygon([coordinates], {}, polygonOptions);
                            map.geoObjects.add(polygon);
        
                            drawButton.disabled = false;
                
                            // На этом этапе переменная coordinates содержит координаты всех точек фигуры
                            $(".mapMenu button").removeClass("active");
                        });
                };
            });
        
            function drawLineOverMap(map) {
                var canvas = document.querySelector('#draw-canvas');
                var ctx2d = canvas.getContext('2d');
                var drawing = false;
                var coordinates = [];
        
                // Задаем размеры канвасу как у карты.
                var rect = map.container.getParentElement().getBoundingClientRect();
                canvas.style.width = rect.width + 'px';
                canvas.style.height = rect.height + 'px';
                canvas.width = rect.width;
                canvas.height = rect.height;
        
                // Применяем стили.
                ctx2d.strokeStyle = canvasOptions.strokeStyle;
                ctx2d.lineWidth = canvasOptions.lineWidth;
                canvas.style.opacity = canvasOptions.opacity;
        
                ctx2d.clearRect(0, 0, canvas.width, canvas.height);
        
                // Показываем канвас. Он будет сверху карты из-за position: absolute.
                canvas.style.display = 'block';
        
                canvas.onmousedown = function (e) {
                    // При нажатии мыши запоминаем, что мы начали рисовать и координаты.
                    drawing = true;
                    coordinates.push([e.offsetX, e.offsetY]);
                };
        
                canvas.onmousemove = function (e) {
                    // При движении мыши запоминаем координаты и рисуем линию.
                    if (drawing) {
                        var last = coordinates[coordinates.length - 1];
                        ctx2d.beginPath();
                        ctx2d.moveTo(last[0], last[1]);
                        ctx2d.lineTo(e.offsetX, e.offsetY);
                        ctx2d.stroke();
        
                        coordinates.push([e.offsetX, e.offsetY]);
                    }
                };
        
                return new Promise(function (resolve) {
                    // При отпускании мыши запоминаем координаты и скрываем канвас.
                    canvas.onmouseup = function (e) {
                        coordinates.push([e.offsetX, e.offsetY]);
                        canvas.style.display = 'none';
                        drawing = false;
        
                        coordinates = coordinates.map(function (x) {
                            return [x[0] / canvas.width, x[1] / canvas.height];
                        });
        
                        resolve(coordinates);
                    };
                });
            }
        
            $(".mapMenu button").on('click', function () {
                $(".mapMenu button").removeClass("active");
                $(this).addClass("active");
            });
        });
    

Вы готовы начать работу или получить консультацию?

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