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