Задача: добавить инструментарий на Яндекс карты для выделения произвольной области.
Яндекс карты обладают огромным количеством возможностей и хорошей документацией. Но, когда необходимо сделать что-то не стандартное, документация не всегда может помочь. Одной из таких задач была реализация для одного из наших клиентов в рамках разработки системы управления клиентами 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");
});
});