Всем привет! Мы рассмотрим библиотеку для построения карт 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, как работает рендер, внутреннее устройство классов, подробнее про интерактив, и так далее. Если у вас остались вопросы, смело оставляйте их в комментариях.
И в завершение хотелось бы узнать, какую библиотеку для построения карт вы используете и почему выбрали именно её?
А я с вами прощаюсь, до новых встреч!