Всем привет! Мы рассмотрим библиотеку для построения карт OpenLayers (версии 8.2.х). Вы узнаете о том, какие возможности она предоставляет, как ей пользоваться и почему в команде НСПД мы выбрали именно её. В статье будет много примеров кода, которые также доступны на GitHub и StackBlitz.
Для чтения статьи необходимо иметь хотя бы базовые знания HTML, CSS и JavaScript; иметь представление о сборщиках кода (в примерах использую Vite).
Приятного чтения! ?
Что такое OpenLayers?

OpenLayers — библиотека для создания карт с открытым исходным кодом (ссылка на исходники кода). Написана на JavaScript, имеет хорошую поддержку Typescript. Является одним из популярнейших решений для работы с картами, конкурируя с Leaflet.
Подход OpenLayers состоит в том, чтобы предоставить пользователю весь необходимый функционал для работы с картами в одной библиотеке, с упором на быстродействие и удобство для конечного пользователя.
Почему OpenLayers?
OpenLayers любят за это:
Быстродействие. Поддержка WebGL из коробки.
Богатый функционал из коробки. Небольшой список я приведу ниже
Архитектурная гибкость. Довольно просто расширять, настраивать под специфические нужды, проводить unit-тестирование.
Отличная поддержка браузеров и девайсов (т.к. мобильные устройства, Retina-дисплей)
Обилие примеров и документации. 240+ примеров
Поддержка всех протоколов OGC, таких как WMF, WFS, WMTS, WPS.
OpenSource. Исходники кода можно (и нужно) читать для лучшего понимания того, что происходит под капотом в Вашем приложении.
Поддержка TreeShaking. В бандл идёт только то, что вы используете (импортируете).
Поддержка любой проекции карты. В библиотеке указаны только EPSG:3857 и EPSG:4326, однако есть возможность добавить любую другую проекцию (пример).
Скрытый текст
- 
Поддержка растровых и и векторных тайлов
Динамическая замена тайлов по зуму (разрешению)
 Работа с векторной графикой по стандарту GeoJSON
- 
Интерактивные взаимодействия с пользователем (Примеры), в том числе:
 - 
Стилизация
 Возможность привязать любой HTML или SVG элемент к любому месту на карте по координатам
Поддержка GeoTIFF
Поддержка D3.js
и многое другое
И всё это из коробки, из одного пакета npm!
Проработав с библиотекой несколько лет, выявилась и пара недостатков:
есть небольшие неудобства в типизации некоторых методов подписки на события (`.on`)
требует очистки памяти после окончания использования на каждом компоненте (issue #14583)
Мы, в команде НСПД, очень довольны этой библиотекой и рекомендуем к ознакомлению и использованию.
Принцип работы
Библиотека построена с на принципах ООП с добавлением событийно-ориентированного. Сам код представляет собой набор классов, которые общаются друг с другом посредством отправки событий.
Благодаря такому подходу, от пользователя скрыты несущественные детали реализации, пользователь библиотеки может легко декомпозировать свой код, сам подписаться на желаемые ему события и получать необходимые данные. Библиотеку просто тестировать.
Однако этот же подход может породить утечки памяти и порождает требование к конечному пользователю не забывать удалять связи между классами, вызывая публичный метод dispose() (issue #14583). Об этом я обязательно напишу отдельную небольшую статью: наша команда уже обожглась с этим моментом и хотелось бы поделиться этим опытом.
Лицензия
OpenLayers распространяется по лицензии FreeBSD или 2-clause BSD. Ссылка на полный текст лицензии. Лицензия всего лишь обязывает сохранять уведомление об авторских правах и позволяет использовать её в коммерческой разработке без раскрытия исходного кода коммерческого продукта.
Практика
Для использования библиотеки необходим любой сборщик модулей, будь то Webpack, Vite, Rollup или любой другой. В своих примерах я буду использовать Vite, он прекрасен.
Для использования OpenLayers, достаточно установить npm пакет командой
npm install ol
Все примеры кода из статьи доступны в онлайн-редакторе StackBlitz, так что для того, чтобы их пощупать совершенно не обязательно устанавливать всё локально. Также весь исходный код примеров доступен на GitHub.

Создаём первую карту
Ссылки на Онлайн-пример и Исходный код.
Создадим простую карту с центром в Москве и тайлами от OpenStreetMap
HTML
<head>
  <!-- Стили по умолчанию от OpenLayers -->
  <link rel="stylesheet" href="/node_modules/ol/ol.css" />
  <!-- Наши стили: сделаем карту во всю ширину страницы -->
  <link rel="stylesheet" href="./style.css" />
</head>
<body>
  <!-- Контейнер для карты -->
  <div id="map"></div>
  <!-- JavaScript код для инициализации карты -->
  <script type="module" src="./index.js"></script>
</body>
Стили
html, body {
 margin: 0;
 height: 100%;
}
#map {
 position: absolute;
 top: 0;
 bottom: 0;
 width: 100%;
}
JavaScript
import { Map, View } from "ol";
import OSM from "ol/source/OSM";
import TileLayer from "ol/layer/Tile";
// Создаём экземпляр карты
const map = new Map({
 // HTML-элемент, в который будет инициализирована карта
 target: document.getElementById("map"),
  
 // Список слоёв на карте
 layers: [
   // Создадим тайловый слой. Источником тайлов будет OpenStreetMap
   new TileLayer({ source: new OSM() }),
 ],
  
 // Параметры отображения карты по умолчанию: координата центра и зум
 view: new View({
   center: [4190701.0645526173, 7511438.408408914],
   zoom: 10,
 }),
});

Каждый установленный параметр затем можно будет изменить в любой момент в ходе выполнения программы
// Можем изменить значения любых переданных параметров
map.setTarget(document.getElementById("#another-place-for-map"));
map.getView().setZoom(5);
map.getView().setCenter([4190701.0645526173, 7511438.408408914]);
// и так далее…
Изменим проекцию
Ссылки на Онлайн-пример и Исходный код.
По умолчанию, карта создается с проекцией EPSG:3857 (Web Mercator). Для того, чтобы изменить систему координат на, допустим, EPSG:4326 (WGS 84), следует обновить вызов View.
new View({
   projection: 'EPSG:4326',
   // ...
}),

Соответственно, изменяя проекцию, изменяется и система координат.
Так, например, если мы хотим центрировать карту на Москву для EPSG:3857 мы используем:
new View({
   // Москва в EPSG:3857 (метры)
   center: [4190701.0645526173, 7511438.408408914],
}),
, а для EPSG:4326:
new View({
   // Москва в EPSG:4326 (градусы)
   center: [37.645708, 55.7632972],
})
По умолчанию, OpenLayers включает в себя лишь указанные выше две проекции. Остальные можно добавить самостоятельно. Подробнее об этом можно прочитать здесь.
Подсказка: как получить координаты точки на карте?
Добавляя прослушку на событие клика, можно легко и быстро узнать координаты определённого места на карте. Напомню, что значение координаты зависит от системы координат используемой проекции.
// Добавляем обработчик клика по карте
map.on("click", function (event) {
 // Кидаем в консоль координаты
 console.log(event.coordinate);
 // Часто бывает полезно знать текущий зум
 console.log(map.getView().getZoom());
});
Подсказка: конвертация координат
Часто бывает так, что координаты могут быть в разных проекциях. Для удобства использования, OpenLayers предоставляет 2 функции для конвертации координат между EPSG:4326 и EPSG:3857:
import { fromLonLat, toLonLat } from "ol/proj";
// Конвертация координат из EPSG:4326 в EPSG:3857
// Вернёт: [4190701.0645526173, 7511438.408408914]
fromLonLat([37.64570817463565, 55.76329720561773]);
// И обратно
// Вернёт: [37.64570817463565, 55.76329720561773]
toLonLat([4190701.0645526173, 7511438.408408914]);
Тайловые слои
В предыдущем примере мы использовали OpenStreetMap, однако, OpenLayers позволяет использовать любой провайдер тайлов. Также добавлю, что библиотека поддерживает как растровые тайлы, так и векторные.
ArcGIS
Ссылки на Онлайн-пример и Исходный код.
Довольно часто приходится менять тайлы для наших приложений. Для примера, давайте добавим тайлы из сервиса ArcGIS.
import { Map, View } from "ol";
import XYZ from "ol/source/XYZ";
import TileLayer from "ol/layer/Tile";
const map = new Map({
 target: document.getElementById("map"),
 layers: [
   new TileLayer({
     source: new XYZ({
       attributions:
         'Tiles © <a href="https://services.arcgisonline.com/ArcGIS/' +
         'rest/services/World_Topo_Map/MapServer">ArcGIS</a>',
       url: "https://server.arcgisonline.com/ArcGIS/rest/services/" + "World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
     }),
   }),
 ],
 view: new View({
   center: [4517934.495704523, -2284501.936731553],
   zoom: 5,
 }),
});

OGC (растровые тайлы)
Ссылки на Онлайн-пример и Исходный код.
import OGCMapTile from 'ol/source/OGCMapTile.js';
import TileLayer from 'ol/layer/Tile.js';
new TileLayer({
  // API не подходит для использования XYZ?
  // Решение: создать свой подкласс и доработать API под свои нужды
  source: new OGCMapTile({
    url: 'https://maps.gnosis.earth/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad',
  }),
}),

OGC (векторные тайлы)
Ссылки на Онлайн-пример и Исходный код.
Преимущество векторных тайлов над растровыми в том, что они:
имеют меньший размер файла
быстрее загружаются
легко стилизуются
не размываются
Этот пример рекомендую смотреть прямо в браузере. Скриншот не отражает той резкости, которую добавляет векторный слой.
import MVT from 'ol/format/MVT.js';
import OGCVectorTile from 'ol/source/OGCVectorTile.js';
import VectorTileLayer from 'ol/layer/VectorTile.js';
new VectorTileLayer({
  source: new OGCVectorTile({
    url: 'https://maps.gnosis.earth/ogcapi/collections/NaturalEarth:cultural:ne_10m_admin_0_countries/tiles/WebMercatorQuad',
    format: new MVT(),
  }),
  background: '#e2e3e3',
  style: {
    'stroke-width': 1,
    'stroke-color': '#8c8b8b',
    'fill-color': '#f7f7e9',
  },
})

Подробнее об этом можно прочесть здесь или в Workshop.
Используем дебаггер тайлов
Ссылки на Онлайн-пример и Исходный код.
import TileLayer from "ol/layer/Tile.js";
import TileDebug from "ol/source/TileDebug";
new TileLayer({ source: new TileDebug() })

Подробнее можно почитать здесь.
Стилизация
Ссылки на Онлайн-пример и Исходный код.
const layer = new TileLayer({
 // Выбираем источник для тайлов
 source: new OGCMapTile({
   url: "https://maps.gnosis.earth/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad",
 }),
 // Можем сделать прозрачным
 opacity: 0.9,
 // Можем скрывать слой, не убирая его из карты (и теряя тем самым настройки)
 visible: true,
 // Можем менять наложение слоёв друг на друга
 zIndex: 5,
 // Можем ограничить слой определённой областью
 extent: [6717612.447527122, 583571.8523972307, 10985130.57817296, 3294022.5569966147],
 // Можем показывать слой лишь в заданных границах зума
 maxZoom: 7,
 // Можем так же показывать слой лишь на определённых разрешениях (масштабе) карты
 // minResolution: 10000,
 // maxResolution: 100000,
});
map.addLayer(layer);
Пример в видео (не удалось загрузить гифку, так как она больше 8мб ?).
Более того, мы можем использовать ещё и CSS стили для этого слоя. Именно для этого я прописал класс «favorite-layer».
const layer = new TileLayer({
 // Можем добавить CSS-стилей
 className: "favorite-layer",
 // ...
});

Так, например, можно добавить эффект размытия
.favorite-layer {
  filter: blur(1px);
}

Оверлей (Overlay)
Часто бывает, что нужно обычный HTML-элемент «привязать» к определенной координате на карте.
Для этих целей существует класс `Overlay`. Он хранит в себе координаты позиции на карте и ссылку на HTML-элемент и их связывает.
Добавляем попап с координатами по клику
В HTML файле мы добавляем такой шаблон
<!-- Шаблон попапа -->
<div style="display: none">
  <div id="popup" class="popup">
    <p>Координаты</p>
    <p><span id="coordinates"></span></p>
  </div>
</div>
В JS файле используем HTML-шаблон
// Попап (без указанного `position` он не будет показан)
const popup = document.getElementById("popup");
const overlay = new Overlay({
 element: popup,
 positioning: "bottom-center",
 offset: [0, -4],
});
map.addOverlay(overlay);
map.on("click", (event) => {
 // Переводим координаты в географические (широта, долгота)
 const [lon, lat] = toLonLat(event.coordinate);
 // Отображаем в формате "широта, долгота"
 const coordsEl = popup.querySelector("#coordinates");
 coordsEl.textContent = createStringXY(6)([lat, lon]);
 // Закрепляем оверлей за координатой
 overlay.setPosition(event.coordinate);
});

Отобразим офис компании, использовав маркер и попап
Довольно часто карту используют только для этих целей ?
HTML
<!-- Шаблон маркера -->
<template id="marker">
 <img width="36" src="/public/icons/marker.svg" alt="Marker" />
</template>
<!-- Шаблон попапа -->
<template id="popup">
 <div class="popup">
   <p>Головной офис БФТ-Холдинг</p>
   <p>129085, г. Москва, ст. м. «Алексеевская» ул. Годовикова, д. 9, стр. 17</p>
 </div>
</template>
JavaScript
// Позиция на карте (головной офис в БФТ)
const position = [4188878.742882752, 7520435.741484543];
// Маркер
const markerTemplate = document.getElementById("marker");
const marker = markerTemplate.content.cloneNode(true);
const markerOverlay = new Overlay({
 element: marker,
 positioning: "bottom-center",
 position: position,
});
map.addOverlay(markerOverlay);
// Попап
const popupTemplate = document.getElementById("popup");
const popup = popupTemplate.content.cloneNode(true);
const popupOverlay = new Overlay({
 element: popup,
 positioning: "bottom-center",
 offset: [0, -36],
 position: position,
});
map.addOverlay(popupOverlay);

Таким образом, в случаях, когда для одной позиции может быть привязано несколько HTML-элементов, мы можем использовать несколько оверлеев.
Кликабельные маркеры на примере аэропортов Москвы
Мне понравилась эта иконка, поэтому решил добавить этот пример ?
HTML
<div style="display: none">
 <!-- Шаблон маркера -->
 <img id="marker" class="marker" width="36" src="/public/icons/airport.svg" alt="Marker" />
 <!-- Шаблон попапа -->
 <div id="popup" class="popup">
   <p id="title"></p>
 </div>
</div>
JavaScript
// Попап
const popup = document.getElementById("popup").cloneNode(true);
const popupOverlay = new Overlay({
 element: popup,
 positioning: "bottom-center",
 offset: [0, -42],
});
map.addOverlay(popupOverlay);
// Функция создания маркеров
// По клику на маркер, покажется попап
function createMarker(position, title) {
 const marker = document.getElementById("marker").cloneNode(true);
 const markerOverlay = new Overlay({
   element: marker,
   positioning: "bottom-center",
   position: position,
 });
 map.addOverlay(markerOverlay);
 marker.addEventListener("click", function () {
   popupOverlay.setPosition(position);
   popup.querySelector("#title").textContent = title;
 });
}
// Создание маркеров аэропортов
// Аэропорты хранятся в .json файле
airports.forEach((airport) => {
 createMarker(airport.position, airport.title);
});

От себя добавлю, что мы можем использовать не только SVG формат, но и PNG, JPEG и прочие. Всё, что можно добавить в HTML, можно закрепить за координатой на карте.
Также добавлю, что для отображения маркера и всплывашки вместо HTML мы могли бы использовать и векторный слой. Или добавить свою render-функцию в Style и рисовать картинку прямо на canvas-элементе.
Векторный слой
OpenLayers даёт широкие возможности для работы с векторной графикой.
Ранее я привёл пример добавления векторного тайлового слоя (`VectorTileLayer`). Векторные тайлы хранятся в базе данных на бэкенде, карта запрашивает и рисует тайлы. Здесь же мы рассмотрим работу именно с геометрией: как её нарисовать, как стилизовать, как добавить интерактив.
Как это работает?
OpenLayers предоставляет нам несколько классов для рисования примитивных геом. фигур:
Point - Точка
LineString - Линия
Polygon - Многоугольник
Circle - Окружность
Из примитивных геометрических фигур складываются сложные, такие как:
MultiPoint - Множество точек
MultiLineString - Множество линий
MultiPolygon - Множество точек
GeometryCollection - Множество разных геометрий
Пример создания геометрии:
// Создаем геометрию точки
const geometry = new Point([6216653.792416765, 2341922.265922518])
Зачастую, с геометрией связаны некие абстрактные данные. Это может быть ID, кадастровый номер, стоимость участка и многое другое. Чтобы связать геометрию на карте и разнородные связанные данные, используется Feature. Грубо говоря, `Feature = Geometry + Properties`.
// Создаем фичу, назначая геометрию (точку, созданную ранее) при инициализации
const feature = new Feature(geometry);
// Можем заменить геометрию "на лету"
feature.setGeometry(geometry);
// Можем сохранить ID, чтобы фичу можно было получить в дальнейшем
feature.setId(12345);
// Можем сохранить абстрактные свойства внутри фичи
feature.setProperties({ ... });
Для хранения фич используется векторное хранилище - VectorSource. Оно необходимо, чтобы абстрагировать источник данных.
// Создаем хранилище векторных данных (фич)
const source = new VectorSource({
  // Можем назначить фичи при инициализации
  features: [feature],
});
// Можем добавить "на лету"
source.addFeature(feature);
// В процессе работы, можем получить желаемую фичи по её ID
source.getFeatureById(12345);
В текущем примере, мы геометрию и фичу создали вручную. Однако, порой удобнее оставить ссылку на API бэкенда, возвращающего фичи, чтобы VectorSource сам их запросил и сохранил в себе.
import KML from "ol/format/KML";
const source = new VectorSource({
  // Ссылка на бэкенд
  url: '/some-api',
  // Можем задать форматтер входящих данных
  // Например, для KML формата
  format: new KML(),
});
// Ссылку можно динамически обновить
source.setUrl('/some-api');
Для того, чтобы отрисовать данные из хранилища данных на карте, нужно использовать векторный слой — VectorLayer. Он нужен, чтобы правильно отрисовать на карте фичи из хранилища.
const layer = new VectorLayer({
 source: source,
 // Можем добавить фон
 background: 'rgba(255, 255, 255, 0.5)',
 // Можем добавить HTML-класс, чтобы застилить слой с помощью CSS
 className: 'my-layer',
 // Можем скрыть/показать
 visible: true,
 // Можем изменить порядок между слоями, подняв/опустив слой относительно других
 zIndex: 5,
 // Меняем уровень прозрачности
 opacity,
 // Можем показывать только в границах заданного зума
 minZoom: 3,
 maxZoom: 15,
 // Или в границах заданного разрешения карты
 minResolution: 10_000,
 maxResolution: 1_000_000,
});
Таким образом, шаг за шагом поднимается уровень абстракции. Цепочка добавления геометрии на карту выглядит так: `Geometry -> Feature -> Source -> Layer`.
Добавим геометрических фигур на карту
Ссылки на Онлайн-пример и Исходный код.
Попробуем нарисовать немного простых геометрических фигур на карте. Для этого нам необходимо использовать векторный слой (VectorLayer) и векторное хранилище (VectorSource).
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
const source = new VectorSource();
const layer = new VectorLayer({ source: source });
// Нарисуем точку
const point = new Feature({ geometry: new Point([6216653.792416765, 2341922.265922518]) });
source.addFeature(point);
// Нарисуем линию
const line = new Feature({
 geometry: new LineString([
   [6098023.524518171, 2417747.7979814108],
   [6195862.920723196, 2569398.8620992005],
   [6290033.3395705335, 2406740.865908345],
 ]),
});
source.addFeature(line);
// Нарисуем полигон
const polygon = new Feature({
 geometry: new Polygon([
   [
     [5929250.566064502, 2369439.5961051816],
     [5857094.011363296, 2479508.916835835],
     [5968386.324546512, 2583463.275303675],
     [6096800.532065609, 2503968.765887092],
     [6096800.532065609, 2503968.765887092],
     [5929250.566064502, 2369439.5961051816],
   ],
 ]),
});
source.addFeature(polygon);
// Нарисуем окружность
const circle = new Feature({
 geometry: new Circle([6401325.652753751, 2559003.4262524187], 100000),
});
source.addFeature(circle);
map.addLayer(layer);

Таким образом, чтобы нарисовать геометрию, нам просто нужно добавить Feature с необходимой геометрией в VectorSource. Карта обновится автоматически и они будут нарисованы. Работать с этим довольно удобно: всё что связанно с данными в одном месте, а всё что связано со стилизацией или группировкой данных - в другом. Главное - не забыть добавить слой на карту ?
Для примера я сохранил в коде несколько координат. На практике, зачастую их предоставляет либо сервер, либо пользователь, загружая файл с координатами (будь-то GeoJSON, KML, ShapeFile) или рисуя на самой карте.
Классы геометрии очень напоминают объекты из стандарта GeoJSON. И не спроста: классы принимают координаты в формате GeoJSON и рисуют соответствующие фигуры из стандарта, что довольно удобно на практике, поскольку и бекенд и фронтенд подчинены одному стандарту. Как пример, в кольцах из линий Polygon-а первая и последняя координаты должны быть равны:
new Polygon([
   [
     [5929250.566064502, 2369439.5961051816],
	 // ... координаты ...
     [5929250.566064502, 2369439.5961051816],
   ],
])
Есть одно исключение - это Circle: мы можем нарисовать окружность, хотя в стандарте GeoJSON никакой окружности не описано.
Мы, как разработчики, можем добавлять неограниченное количество фигур. Вот пример всей карты в интерактивных векторах. Единственное ограничение для нас - мощность машины пользователя. Обилие векторов может сильно нагрузить процессор и оперативную память пользователя, и это необходимо помнить.
Стилизация
Ссылки на Онлайн-пример и Исходный код.
Попробуем добавить других стилей для всех фич внутри слоя.
new VectorLayer({
  // Для стилизации используем класс Style
  style: new Style({
    // Заливка
    fill: new Fill({ color: "rgba(230, 161, 79, 0.4)" }),
    // Обводка
   stroke: new Stroke({ color: "rgba(230, 161, 79, 1)", width: 2 }),
    // Стиль для точки
    image: new CircleStyle({
      radius: 6,
      fill: new Fill({ color: "rgba(230, 161, 79, 0.4)" }),
      stroke: new Stroke({ color: "rgba(230, 161, 79, 1)", width: 2 }),
    }),
 }),
});

Свойство `color` может быть любым свойством, которое принимает CanvasRenderingContext2D.fillStyle, т.е. цветом, градиентом или паттерном. Ссылка на более сложный пример здесь.
В примере выше мы добавили стили для всего слоя. Они применяются для всех фич внутри него. Однако, часто бывают ситуации, когда лишь одна или несколько фич внутри слоя требуют других стилей. В таком случае, мы можем стилизовать каждую фичу отдельно.
Попробуем добавить стили только для полигона
polygon.setStyle([
 // Стили для полигона
 new Style({
   fill: new Fill({ color: "rgba(193, 211, 63, 0.4)" }),
   stroke: new Stroke({ color: "rgba(193, 211, 63, 1)", width: 2 }),
 }),
 // Стили для вершин
 new Style({
   image: new CircleStyle({
     radius: 6,
     fill: new Fill({ color: "rgba(255, 255, 255, 0.7)" }),
     stroke: new Stroke({ color: "rgba(147, 211, 63, 1)", width: 4 }),
   }),
   // Чтобы стилизовать часть геометрии, достаточно получить желаемые части
   // Это могут быть грани для полигона или линий, центр для окружности, вершины полигона
   geometry: function (feature) {
     // Получаем все вершины
     const coordinates = feature.getGeometry().getCoordinates()[0];
     return new MultiPoint(coordinates);
   },
 }),
]);

Более подробно о стилизации напишу в отдельной статье, а пока что идем дальше.
Взаимодействие с геометрией
Здесь мы рассмотрим классы из группы `ol/interaction/…` — классы взаимодействий.
Эти классы дают возможность пользователю непосредственно взаимодействовать с векторными данными: рисовать, изменять, выбирать (кликом или наведением мышкой), использовать Drag’n’Drop и прочее.
Примеры взаимодействий можно посмотреть по ссылке.
Рисование
Попробуем добавить пользователю возможность нарисовать геометрию на карте
import { Draw } from 'ol/interaction';
// Draw - класс для рисования
const interaction = new Draw({
 type: "Polygon",
 source: source,
});
map.addInteraction(interaction);
// Колбек на завершение рисовании фичи
interaction.on("drawend", (event) => {
 const geometry = event.feature.getGeometry();
 console.log({
   type: geometry.getType(),
   coordinates: geometry.getCoordinates(),
 });
});

Редактирование геометрии
Попробуем добавить пользователю возможность изменять уже существующую геометрию на карте.
import { Modify } from 'ol/interaction';
// Modify - класс для редактирования
const interaction = new Modify({
 // Мы можем передать VectorSource или список фич
 source: source,
});
map.addInteraction(interaction);
interaction.on('modifyend', (event) => {
 console.log(event.features);
});

Выделение
Попробуем совместить получение знания по Overlay и добавим его при выделении объекта.
import { Select } from 'ol/interaction';
// Select - позволяет пользователю выделять кликом мыши геометрию на карте
const interaction = new Select({
 // Мы можем передать VectorSource или список фич
 source: source,
 style: new Style({
   fill: new Fill({ color: "rgba(255, 255, 255, 0.5)" }),
   stroke: new Stroke({ color: "#674ea7", width: 4 }),
   image: new CircleStyle({
     radius: 6,
     fill: new Fill({ color: "#674ea7" }),
     stroke: new Stroke({ color: "#fff", width: 4 }),
   }),
 }),
});
map.addInteraction(interaction);
// Попап
const popup = document.getElementById("popup").cloneNode(true);
const popupOverlay = new Overlay({
 element: popup,
 positioning: "bottom-center",
 offset: [0, 0],
});
map.addOverlay(popupOverlay);
// Опционально: добавим оверлей сверху над выделенной геометрией
interaction.on("select", (event) => {
 const feature = event.selected[0];
 if (!feature) {
   popupOverlay.setPosition(undefined);
   return;
 }
 const center = getCenter(feature.getGeometry().getExtent());
 const title = feature.getProperties().name;
 popup.querySelector("#title").textContent = title;
 popupOverlay.setPosition(center);
});

Контролы (Controls)
Контрол — видимый пользователю интерактивный элемент, который находится в фиксированном положении на карте (зачастую в углах или на краях).
Давайте попробуем использовать как уже готовые контролы из библиотеки, так и создать полностью свой.
Как добавить контрол на карту?
Добавление контрола, так же как и в случае с другими инструментами карты, может происходить либо в конструкторе, либо по вызову `addControl`.
Важно помнить, что по умолчанию, OpenLayers инициализируется с несколькими контролами, в числе которых Zoom, Rotate, Attribution. И если мы хотим их оставить, то необходимо это явно указать.
import { defaults } from "ol/control";
new Map({
  // Мы можем оставить контролы по умолчанию
  controls: defaults().extend([ new ScaleLine() ]),
  // Или не оставлять
  controls: [new ScaleLine()],
});
// Мы можем добавить/убрать контрол в любой момент
map.addControl(new ScaleLine());
Полный список доступных по умолчанию контролов можно посмотреть в документации к API OpenLayers (в поиске следует ввести `ol/control`)
Перемещение пользователя к определённому месту на карте
Ссылки на Онлайн-пример и Исходный код.
Попробуем использовать встроенный контрол ZoomToExtent, чтобы по клику пользователь перемещался в Москву
import { ZoomToExtent } from "ol/control.js";
new ZoomToExtent({
  extent: [
    4076072.4828566443,
    7450792.337368891,
    4300910.783649025,
    7554077.43179539
  ],
  label: "М",
  tipLabel: "Переместиться в Москву",
})


Подробнее об этом контроле можно почитать здесь.
Анимируем контрол из примера выше
Ссылки на Онлайн-пример и Исходный код.
Хотя предыдущий контрол делает свою работу, но всё же что-то не то. Выглядит резко и не очень приятно. Давайте добавим анимацию, расширив изначальный класс контрола.
Наконец-то нашёл место, где уместно показать расширение через наследование ?
import { ZoomToExtent } from "ol/control.js";
import { easeOut } from "ol/easing";
class ZoomToExtentWithAnimation extends ZoomToExtent {
  /**
   * @protected
   */
  handleZoomToExtent() {
    const view = this.getMap().getView();
    const extent = this.extent || view.getProjection().getExtent();
    // Добавляем анимацию
    view.fit(extent, { duration: 300, easing: easeOut });
  }
}
new ZoomToExtentWithAnimation({
  extent: [4076072.4828566443, 7450792.337368891, 4300910.783649025, 7554077.43179539],
  label: "М",
  tipLabel: "Переместиться в Москву",
});
Получается более приятная анимация перехода к заданному месту

Анимацию можно настроить на свой вкус, заменив время, функцию анимации, максимальный зум и прочие опции. Полный список возможных параметров можно посмотреть здесь.
Миникарта
Ссылки на Онлайн-пример и Исходный код.
Для этого используем контрол из библиотеки - OverviewMap.
import TileLayer from "ol/layer/Tile.js";
import OSM from "ol/source/OSM.js";
import { OverviewMap } from "ol/control.js";
const сontrol = new OverviewMap({
 // На эти классы добавим CSS (в примере style.css)
 className: "ol-overviewmap ol-custom-overviewmap",
 layers: [new TileLayer({ source: new OSM() })],
 collapseLabel: "\u00BB",
 label: "\u00AB",
 collapsed: false,
});

Миникарта, как и многие контролы, легко стилизуема. Мы можем заменить контейнер на любой HTML-элемент, вынося таким образом миникарту в любое место страницы. Можно заменить тайловый слой и стили.
Немного поигравшись со стилями легко получаем следующий пример комбинации OSM-слоя на основной карте и StadiaMaps в миникарте со скруглёнными краями.
Ссылки на Онлайн-пример и Исходный код.

Подробнее об этом контроле можно почитать здесь.
Отобразим текущие координаты под курсором
Ссылки на Онлайн-пример и Исходный код.
import MousePosition from "ol/control/MousePosition.js";
const mousePositionControl = new MousePosition({
  // Количество цифр после запятой
  coordinateFormat: createStringXY(4),
  projection: "EPSG:4326",
  className: "control-coordinates ol-unselectable ol-control",
  target: document.querySelector("#map .ol-overlaycontainer-stopevent"),
});
map.addControl(mousePositionControl);

Подробнее об этом контроле можно почитать здесь.
Геолокация
Ссылки на Онлайн-пример и Исходный код.
Вот мы подошли к тому, чтобы создать полностью свой, кастомный контрол. Для этого следует наследоваться от класса Control и добавить свое поведение.
import Feature from "ol/Feature.js";
import Geolocation from "ol/Geolocation.js";
import Point from "ol/geom/Point.js";
import { Circle as CircleStyle, Fill, Stroke, Style } from "ol/style.js";
import { Vector as VectorSource } from "ol/source.js";
import { Vector as VectorLayer } from "ol/layer.js";
import Control from "ol/control/Control";
/**
* Создаем свой класс контрола как дочерний от Control
*/
export class GeolocationControl extends Control {
 // При обновлении геометрии в фичах, автоматически произойдёт перерисовка слоя карты
 accuracyFeature = new Feature();
 positionFeature = new Feature();
 layer = new VectorLayer({ source: new VectorSource({ features: [this.accuracyFeature, this.positionFeature] }) });
 constructor() {
   const element = document.createElement("div");
   super({ element: element });
   // Подготавливаем вёрстку
   // Иконка взята из https://icons8.com/icons/set/location
   element.className = "control-geolocation ol-unselectable ol-control";
   const button = document.createElement("button");
   button.innerHTML = '<img src="https://img.icons8.com/ios/50/marker--v1.png" alt="marker--v1"/>';
   element.appendChild(button);
   button.addEventListener("click", this._handleClick.bind(this));
   // Подготавливаем класс, отслеживающий геолокацию
   // Является оберткой над Geolocation API
   // @see https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API
   const geolocation = new Geolocation({ trackingOptions: { enableHighAccuracy: true } });
   geolocation.on("error", (error) => {
     alert(error.message);
   });
   geolocation.on("change:accuracyGeometry", () => {
     this.accuracyFeature.setGeometry(geolocation.getAccuracyGeometry());
   });
   geolocation.on("change:position", () => {
     const coordinates = geolocation.getPosition();
     const geometry = coordinates ? new Point(coordinates) : null;
     this.positionFeature.setGeometry(geometry);
     if (geometry) {
       this.getMap()?.getView().fit(geometry, { duration: 300, maxZoom: 13 });
     }
   });
   // Добавляем чуть более красивых стилей
   this.positionFeature.setStyle(
     new Style({
       image: new CircleStyle({
         radius: 6,
         fill: new Fill({ color: "#3399CC" }),
         stroke: new Stroke({ color: "#fff", width: 2 }),
       }),
     })
   );
   this.geolocation = geolocation;
   this.button = button;
 }
 /**
  * @overrides
  * Метод вызывает OpenLayers при встраивании контрола в карту
  */
 setMap(map) {
   super.setMap(map);
   if (map) {
     this.geolocation.setProjection(map.getView().getProjection());
     map.addLayer(this.layer);
   }
 }
 /**
  * Обработчик клика по кнопке контрола
  */
 _handleClick() {
   this.geolocation.setTracking(true);
 }
}


Стоит обратить внимание на стиль самой точки на карте — она изображена в виде иконки.
new Style({
 image: new Icon({
   width: 36,
   src: "/public/icons/human.svg",
 }),
});
Мы можем любую точку векторного слоя на карте отобразить как иконку. Точно так же и вершины полигона можно изобразить в виде любой иконки.
Подробнее о классах Geolocation и Control можно почитать, перейдя по ссылкам.
Заключение
Мы рассмотрели и попробовали на практике библиотеку OpenLayers. В дальнейшем я планирую написать ещё несколько статей на более специфичные темы: работа с GeoTIFF, как работает рендер, внутреннее устройство классов, подробнее про интерактив, и так далее. Если у вас остались вопросы, смело оставляйте их в комментариях.
И в завершение хотелось бы узнать, какую библиотеку для построения карт вы используете и почему выбрали именно её?
А я с вами прощаюсь, до новых встреч!
Комментарии (4)

kspshnik
07.09.2024 16:12+1А в
import { Map, View } from "ol";перегрузка встроенного класса - не пугает?

filippov70
07.09.2024 16:12+1позанудствую...
EPSG:4326 это не проекция, этого географическая система координта с угловыми координатами в градусах на эллипсоиде WGS-84
          
 
VanishingPoint
Работал с Leaflet. Здорово намучился когда пытался разобраться с кластеризацией маркеров. Есть какой-то leaflet MarkerCluster, но он работает плохо, под какие-то старые версии, в общем, так и забил.
А тут это есть из коробки или еще откуда-нибудь?
Ну смысл такой, что при zoom-out, маркеры (особенно если их много) должны не отрисовываться все, а группироваться в кластеры, которые показываются в виде окружности. Чем больше в кластере маркеров, тем больше диаметр окружности, а по центру в ней - количество маркеров цифрой. Надеюсь понятно объяснил.