Leaflet. Дружим Image с Canvas

от автора

Leaflet Map

Доброго времени суток, дорогие хабрахабровцы!

Leaflet — библиотека, позволяющая добавить интерактивные карты на Ваш сайт и легко их кастомизировать. Сегодня рассмотрим то, как можно разместить изображения на Canvas-слое карт, совместно с базовыми маркерами.

Задача

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

  • Для оптимизации карты, рендеринг объектов должен происходить с использованием Canvas.
  • Маркеры могут быть двух типов: точки и изображения.
  • Если маркеры перекрывают друг друга — то сверху должен оказаться маркер более приоритетного статуса.
  • Каждый маркер должен быть активным при наведении на него мышкой (например для вывода дополнительной информации).

Подготовка

Подключим библиотеку Leaflet.js и добавим базовую карту.

const map = L.map('map', {     preferCanvas: true, }).setView([51.505, -0.09], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);

Для наглядности будем использовать 3 состояния в порядке увеличение приоритета: базовый (зеленый маркер), сообщение (изображение) и ошибка (красный маркер).

Соответственно, красный маркер должен перекрывать изображение, а изображение — перекрывать зеленый маркер.

/* Базовый маркер */ L.circleMarker(L.latLng(51.52, -0.109), {     radius: 10,     fillColor: '#27ae60',     fillOpacity: 1,     color: '#fff',     weight: 3, }).addTo(map);  /* Маркер сообщения */ L.marker(L.latLng(51.52, -0.109), {     icon: L.icon({         iconUrl: 'icon.png',   // url картинки         iconSize: [40, 40],   // размер маркера         iconAnchor: [20, 20],   // выравнивание относительно центра     }), }).addTo(map);  /* Маркер ошибки */ L.circleMarker(L.latLng(51.52, -0.109), {     radius: 8,     fillColor: '#f44334',     fillOpacity: 1,     color: '#fff',     weight: 3, }).addTo(map);

Проблема

Leaflet добавляет маркеры поочередно, поэтому каждый последующий должен перекрывать предыдущий. Но на деле это не так. L.marker добавляет изображение в качестве обыкновенного IMG, отдельно от слоя Canvas.

Его можно разместить либо перед, либо под Canvas. И как следствие, невозможно поместить L.marker между двух L.circleMarker.

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

Примечание: В сети есть несколько плагинов, позволяющих добавлять изображения на Canvas. Но они создают отдельный Canvas, или даже группу слоев! В итоге простое размещение маркеров по приоритету становится довольно затруднительным. А так же Canvas-слои перекрывают друг друга, и кликнуть мышкой на маркер нижестоящего слоя становится невозможным!

Решение

Шаг 1. Создаем дочерний класс от L.CircleMarker, который будет получать объект ‘img’, загружать изображение и добавлять его в L.Canvas.

const CanvasMarker = L.CircleMarker.extend({     _updatePath() {         if (!this.options.img.el) { //Создаем элемент IMG             const img = document.createElement('img');             img.src = this.options.img.url;             this.options.img.el = img;             img.onload = () => {                 this.redraw();  //После загрузки запускаем перерисовку             };         } else {             this._renderer._updateImg(this);    //Вызываем _updateImg         }     }, });  L.canvasMarker = function (...options) {     return new CanvasMarker(...options); };

Шаг 2. Описываем метод _updateImg в L.Canvas. Он получает объект с изображением, который мы передаем на Шаге 1 и рисует его на Canvas.

L.Canvas.include({     _updateImg(layer) { //Метод добавления img на Canvas-слой         const { img } = layer.options;         const p = layer._point.round();         this._ctx.drawImage(img.el, p.x - img.size[0] / 2, p.y - img.size[1] / 2, img.size[0], img.size[1]);     }, });

Шаг 3. Теперь вместо L.marker можно использовать L.canvasMarker. Обратите внимание, что параметр ‘anchor’ не используется, т.к. картинка выравнивается автоматически!

/* Базовый маркер */     L.circleMarker(L.latLng(51.52, -0.109), {         radius: 10,         fillColor: '#27ae60',         fillOpacity: 1,         color: '#fff',         weight: 3,     }).addTo(map);      /* Маркер сообщения */     L.canvasMarker(L.latLng(51.52, -0.109), {         img: {             url: 'icon.png',             size: [40, 40],         },     }).addTo(map);      /* Маркер ошибки */     L.circleMarker(L.latLng(51.52, -0.109), {         radius: 8,         fillColor: '#f44334',         fillOpacity: 1,         color: '#fff',         weight: 3,     }).addTo(map);

В результате:

  • Все маркеры расположены на едином Canvas-слое.
  • Маркеры перекрывают друг-друга в порядке их добавления на карту.
  • При наведении на маркеры мышкой, они сохраняют активность.

Задача решена!


Дополнительно

Давайте «прокачаем» наш метод L.canvasMarker и добавим возможность автоматически разворачивать изображение в направлении движения по карте!

За основу возьмем координаты предыдущей точки. Для этого сначала доработаем метод _updateImg.

L.Canvas.include({     _updateImg(layer) {         const { img } = layer.options;         const p = layer._point.round();         if (img.rotate) {             this._ctx.save();             this._ctx.translate(p.x, p.y);             this._ctx.rotate(img.rotate * Math.PI / 180);             this._ctx.drawImage(img.el, -img.size[0] / 2, -img.size[1] / 2, img.size[0], img.size[1]);             this._ctx.restore();         } else {             this._ctx.drawImage(img.el, p.x - img.size[0] / 2, p.y - img.size[1] / 2, img.size[0], img.size[1]);         }     }, });

Как видно из примера, для поворота у ‘img’ должно быть свойство ‘rotate’. И мы уже можем задать его вручную при добавлении маркера:

L.canvasMarker(L.latLng(51.52, -0.109), {     img: {         url: 'icon.png',         size: [40, 40],         rotate: 15, //угол поворота изображения     }, }).addTo(map);

Но нам нужно вычислять угол поворота автоматически на основе предыдущей точки. Поэтому добавим вычисление угла на основе двух координат (angleCrds):

 const angleCrds = (map, prevLatlng, latlng) => {     if (!latlng || !prevLatlng) return 0;     const pxStart = map.latLngToLayerPoint(prevLatlng);     const pxEnd = map.latLngToLayerPoint(latlng);     return Math.atan2(pxStart.y - pxEnd.y, pxStart.x - pxEnd.x) / Math.PI * 180 - 90; };  const CanvasMarker = L.CircleMarker.extend({     _updatePath() {         if (!this.options.img.el) {             /* Вызываем метод */             if (!this.options.img.rotate) this.options.img.rotate = 0;             this.options.img.rotate += angleCrds(this._map, this.options.prevLatlng, this._latlng);              const img = document.createElement('img');             img.src = this.options.img.url;             this.options.img.el = img;             img.onload = () => {                 this.redraw();             };         } else {             this._renderer._updateImg(this);         }     }, });  L.canvasMarker(L.latLng(51.52, -0.109), {     prevLatlng: L.latLng(51.528, -0.1), // Координаты предыдущей точки     img: {         url: 'icon.png',         size: [40, 40],     }, }).addTo(map);


Заключение

→ Пример работы можно увидеть здесь
→ Весь описанный функционал я вынес в отдельный npm-плагин

Этот плагин легко подключить и использовать в своих проектах! Так же плагин поддерживает дополнительные настройки, не описанные в данной статье.

Спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/post/485698/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *