Метки на картах это просто. OpenLayers
от aNNiMON
Я всегда считал, что отображать что-то на картах — дело трудоёмкое и непростое. Чтобы разобраться в API нужно посмотреть примеры, а их мало кто выставляет публично, потому что нужен токен для отображения и работы с картой. А токен надо сначала где-то получить, а потом ещё и скрыть от чужих глаз. В общем, морока да и только.
Но потом я подумал, а зачем же мне проприетарные Google Maps, когда есть OpenStreetMaps? Поискал API, наткнулся на OpenLayers, открыл примеры и удивился их количеству и простоте.
В серии статей мы разберёмся с OpenLayers, выведем на карту превьюшки фотографий, прикрутим кластеризацию, а в финале сделаем приложение на Rust, которое соберёт геоданные и превью из фотографий и сгенерирует карту.
Слой 1. OpenStreetMap
Начнём с самого первого слоя — карты. Это будет OpenStreetMap, так как она не требует никаких токенов.
В HTML подключим библиотеку OpenLayers и её стили:
В js создадим слой OpenStreetMap и объект карты:
В параметрах карты нужно указать id html-элемента в target, перечислить слои в layers, а также настроить вид view.
Код очень простой, кроме вот этой строчки:
Здесь я центрирую карту по Европе, а именно 47° северной широты и 20° восточной долготы из проекции EPSG:4326 в стандарт веб карт — проекцию EPSG:385. Каждый раз, работая с системой GPS координат, придётся делать это преобразование.
Этого примера достаточно, чтобы отобразить карту:
📄 Исходный код
Слой 2. Превью фото
Добавим контейнер для объектов меток. Это будет ol.source.Vector:
Создадим слой с превьюшками:
Этот слой будет брать превьюшки из вектора photosSource, а отображать будет в стиле, заданном в функции thumbStyle.
Стилизация нужна, чтобы указать, что именно мы будем отрисовывать и с каким размером. В данном случае рисовать будем иконки с некоторым url, взятым из feature.
Что за feature? ol.Feature это географический объект, отображаемый на карте (позиция, площадь, территория). Он может быть привязан к какому-либо слою и содержит атрибуты, стили и геометрию.
Для отображения фото нам нужны такие данные:
1. Координаты метки.
2. Ссылка на изображение (необязательно, можно просто рисовать точки).
3. Детали (необязательно), которые будут отображаться при выборе метки.
Сбором данных займёмся в следующих статьях. А пока что посмотрим на пример метки, которую я взял из фотоальбомов:
Это json массив с одним элементом. Грузить его будем прямо из кода:
В handleMapDataLoaded пробегаемся по загруженным элементам, создаём объект ol.Feature, преобразуем координаты в проекцию карты и в качестве геометрии выставляем просто точку ol.geom.Point. Затем добавляем объект в photosSource.
Всё, что остаётся сделать, это добавить слой к карте:
Демо
📄 Исходный код
Детали при нажатии
Мелкие фотографии рассматривать не очень удобно, поэтому соберём ещё немного данных о метке. Например, модель камеры, ссылку на оригинал и дату:
При нажатии на фотографию на карте будем показывать детали в отдельной панели:
При помощи ol.interaction.Select можно добавить обработчики выбора объектов на карте, а также задать им соответствующие стили:
Выбор будет происходить на слое layerThumbs. Масштаб выбранной фотографии будет слегка увеличен.
Теперь добавим обработчики выбора и отмены выбора:
При выборе метки в #photo-details вставляем HTML с заменёнными параметрами, перечисленными в массиве const keys = ['name', 'preview', 'date', 'lat', 'lon', 'make', 'model', 'path'];.
Демо
📄 Исходный код
Как видно, если фотографий много, они накладываются друг на друга, поэтому в следующей статье добавим кластеризацию.
Но потом я подумал, а зачем же мне проприетарные Google Maps, когда есть OpenStreetMaps? Поискал API, наткнулся на OpenLayers, открыл примеры и удивился их количеству и простоте.
В серии статей мы разберёмся с OpenLayers, выведем на карту превьюшки фотографий, прикрутим кластеризацию, а в финале сделаем приложение на Rust, которое соберёт геоданные и превью из фотографий и сгенерирует карту.
Слой 1. OpenStreetMap
Начнём с самого первого слоя — карты. Это будет OpenStreetMap, так как она не требует никаких токенов.
В HTML подключим библиотеку OpenLayers и её стили:
- <html>
- <head>
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/ol.css">
- <style>html, body, .map {
- padding: 0;
- margin: 0;
- width: 100%;
- height: 100%;
- }</style>
- </head>
- <body>
- <div id="map" class="map"></div>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ol.js"></script>
- <script src="map.js"></script>
- </body>
- </html>
В js создадим слой OpenStreetMap и объект карты:
- const layerOSM = new ol.layer.Tile({
- source: new ol.source.OSM()
- });
- const map = new ol.Map({
- target: 'map',
- layers: [layerOSM],
- view: new ol.View({
- center: ol.proj.transform([20, 47], 'EPSG:4326', 'EPSG:3857'),
- zoom: 4
- })
- });
Код очень простой, кроме вот этой строчки:
- center: ol.proj.transform([20, 47], 'EPSG:4326', 'EPSG:3857'),
Этого примера достаточно, чтобы отобразить карту:
📄 Исходный код
Слой 2. Превью фото
Добавим контейнер для объектов меток. Это будет ol.source.Vector:
- const photosSource = new ol.source.Vector();
- const layerThumbs = new ol.layer.Vector({
- source: photosSource,
- style: thumbStyle,
- });
- // -- icon styles --
- const cache = {};
- function photoStyle(feature, scale) {
- const url = feature.get('url');
- const key = `${scale}${url}`;
- if (!cache[key]) {
- cache[key] = new ol.style.Style({
- image: new ol.style.Icon({src: url, scale})
- });
- }
- return cache[key];
- }
- function thumbStyle(feature, resolution) {
- return [photoStyle(feature, clamp(0.2, 0.1 / resolution, 1))];
- }
- function clamp(min, value, max) {
- if (value < min) return min;
- if (value > max) return max;
- return value;
- }
Стилизация нужна, чтобы указать, что именно мы будем отрисовывать и с каким размером. В данном случае рисовать будем иконки с некоторым url, взятым из feature.
- const url = feature.get('url');
- // ..
- cache[key] = new ol.style.Style({
- image: new ol.style.Icon({src: url, scale})
- })
Что за feature? ol.Feature это географический объект, отображаемый на карте (позиция, площадь, территория). Он может быть привязан к какому-либо слою и содержит атрибуты, стили и геометрию.
Для отображения фото нам нужны такие данные:
1. Координаты метки.
2. Ссылка на изображение (необязательно, можно просто рисовать точки).
3. Детали (необязательно), которые будут отображаться при выборе метки.
Сбором данных займёмся в следующих статьях. А пока что посмотрим на пример метки, которую я взял из фотоальбомов:
- [{
- "preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
- "lat": 47.765957,
- "lon": 37.255646
- }]
- const handleMapDataLoaded = items => {
- const transform = ol.proj.getTransform('EPSG:4326', 'EPSG:3857');
- items.forEach(item => {
- const feature = new ol.Feature(item);
- feature.set('url', item.preview);
- const coordinate = transform([parseFloat(item.lon), parseFloat(item.lat)]);
- feature.setGeometry(new ol.geom.Point(coordinate));
- photosSource.addFeature(feature);
- });
- };
- handleMapDataLoaded([{
- "preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
- "lat": 47.765957,
- "lon": 37.255646
- }]);
Всё, что остаётся сделать, это добавить слой к карте:
- const map = new ol.Map({
- target: 'map',
- layers: [layerOSM, layerThumbs],
- ...
Демо
📄 Исходный код
Детали при нажатии
Мелкие фотографии рассматривать не очень удобно, поэтому соберём ещё немного данных о метке. Например, модель камеры, ссылку на оригинал и дату:
- handleMapDataLoaded([{
- "name": "dsc_1337_ps_5996.jpg",
- "path": "https://annimon.com/albums/files/dsc_1337_ps_5996.jpg",
- "preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
- "lat": 47.765957,
- "lon": 37.255646,
- "make": "Sony Ericsson",
- "model": "MK16i",
- "date": "2017-06-02 19:30:08"
- }]);
При нажатии на фотографию на карте будем показывать детали в отдельной панели:
- <div id="map" class="map"></div>
- <div id="photo-details"></div>
- <script type="text/html" id="photo-template">
- <div class="photo-details-container">
- <a href="{path}" target="_blank" title="Click to open photo in new tab">
- <img src="{preview}" class="photo-thumbnail">
- </a>
- <div class="photo-details-content">
- <h3>{name}</h3>
- <p><b>Camera:</b> {make} {model}</p>
- <p><b>Date:</b> {date}</p>
- </div>
- </div>
- </script>
При помощи ol.interaction.Select можно добавить обработчики выбора объектов на карте, а также задать им соответствующие стили:
- const thumbSelector = new ol.interaction.Select({
- layers: [layerThumbs],
- style: selectedStyle
- });
- map.addInteraction(thumbSelector);
- function selectedStyle(feature, resolution) {
- return [photoStyle(feature, clamp(0.4, 0.14 / resolution, 1.2))];
- }
Теперь добавим обработчики выбора и отмены выбора:
- const selectedFeatures = thumbSelector.getFeatures();
- selectedFeatures.on('add', event => {
- const feature = event.target.item(0);
- const details = photoDetails(feature);
- document.getElementById('photo-details').innerHTML = details;
- });
- selectedFeatures.on('remove', () => {
- document.getElementById('photo-details').innerHTML = '';
- });
- function photoDetails(feature) {
- let content = document.getElementById('photo-template').innerHTML;
- const keys = ['name', 'preview', 'date', 'lat', 'lon', 'make', 'model', 'path'];
- keys.forEach(key => {
- const value = feature.get(key);
- content = content.replace(`{${key}}`, value);
- });
- return content;
- }
Демо
📄 Исходный код
Как видно, если фотографий много, они накладываются друг на друга, поэтому в следующей статье добавим кластеризацию.