Метки на картах это просто. OpenLayers

от
Прочие языки    openlayers, javascript, maps, карты, геометки

Я всегда считал, что отображать что-то на картах — дело трудоёмкое и непростое. Чтобы разобраться в API нужно посмотреть примеры, а их мало кто выставляет публично, потому что нужен токен для отображения и работы с картой. А токен надо сначала где-то получить, а потом ещё и скрыть от чужих глаз. В общем, морока да и только.

Но потом я подумал, а зачем же мне проприетарные Google Maps, когда есть OpenStreetMaps? Поискал API, наткнулся на OpenLayers, открыл примеры и удивился их количеству и простоте.
logo
В серии статей мы разберёмся с OpenLayers, выведем на карту превьюшки фотографий, прикрутим кластеризацию, а в финале сделаем приложение на Rust, которое соберёт геоданные и превью из фотографий и сгенерирует карту.

Слой 1. OpenStreetMap
Начнём с самого первого слоя — карты. Это будет OpenStreetMap, так как она не требует никаких токенов.

В HTML подключим библиотеку OpenLayers и её стили:
  1. <html>
  2. <head>
  3. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/ol.css">
  4. <style>html, body, .map {
  5.   padding: 0;
  6.   margin: 0;
  7.   width: 100%;
  8.   height: 100%;
  9. }</style>
  10. </head>
  11. <body>
  12.   <div id="map" class="map"></div>
  13.   <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ol.js"></script>
  14.   <script src="map.js"></script>
  15. </body>
  16. </html>

В js создадим слой OpenStreetMap и объект карты:
  1. const layerOSM = new ol.layer.Tile({
  2.   source: new ol.source.OSM()
  3. });
  4. const map = new ol.Map({
  5.   target: 'map',
  6.   layers: [layerOSM],
  7.   view: new ol.View({
  8.     center: ol.proj.transform([20, 47], 'EPSG:4326', 'EPSG:3857'),
  9.     zoom: 4
  10.   })
  11. });
В параметрах карты нужно указать id html-элемента в target, перечислить слои в layers, а также настроить вид view.

Код очень простой, кроме вот этой строчки:
  1. center: ol.proj.transform([20, 47], 'EPSG:4326', 'EPSG:3857'),
Здесь я центрирую карту по Европе, а именно 47° северной широты и 20° восточной долготы из проекции EPSG:4326 в стандарт веб карт — проекцию EPSG:385. Каждый раз, работая с системой GPS координат, придётся делать это преобразование.

Этого примера достаточно, чтобы отобразить карту:

📄 Исходный код


Слой 2. Превью фото
Добавим контейнер для объектов меток. Это будет ol.source.Vector:
  1. const photosSource = new ol.source.Vector();
Создадим слой с превьюшками:
  1. const layerThumbs = new ol.layer.Vector({
  2.   source: photosSource,
  3.   style: thumbStyle,
  4. });
  5.  
  6. // -- icon styles --
  7. const cache = {};
  8.  
  9. function photoStyle(feature, scale) {
  10.   const url = feature.get('url');
  11.   const key = `${scale}${url}`;
  12.   if (!cache[key]) {
  13.     cache[key] = new ol.style.Style({
  14.       image: new ol.style.Icon({src: url, scale})
  15.     });
  16.   }
  17.   return cache[key];
  18. }
  19.  
  20. function thumbStyle(feature, resolution) {
  21.   return [photoStyle(feature, clamp(0.2, 0.1 / resolution, 1))];
  22. }
  23.  
  24. function clamp(min, value, max) {
  25.   if (value < min) return min;
  26.   if (value > max) return max;
  27.   return value;
  28. }
Этот слой будет брать превьюшки из вектора photosSource, а отображать будет в стиле, заданном в функции thumbStyle.

Стилизация нужна, чтобы указать, что именно мы будем отрисовывать и с каким размером. В данном случае рисовать будем иконки с некоторым url, взятым из feature.
  1. const url = feature.get('url');
  2. // ..
  3. cache[key] = new ol.style.Style({
  4.   image: new ol.style.Icon({src: url, scale})
  5. })

Что за feature? ol.Feature это географический объект, отображаемый на карте (позиция, площадь, территория). Он может быть привязан к какому-либо слою и содержит атрибуты, стили и геометрию.

Для отображения фото нам нужны такие данные:
  1. Координаты метки.
  2. Ссылка на изображение (необязательно, можно просто рисовать точки).
  3. Детали (необязательно), которые будут отображаться при выборе метки.

Сбором данных займёмся в следующих статьях. А пока что посмотрим на пример метки, которую я взял из фотоальбомов:
  1. [{
  2.   "preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
  3.   "lat": 47.765957,
  4.   "lon": 37.255646
  5. }]
Это json массив с одним элементом. Грузить его будем прямо из кода:
  1. const handleMapDataLoaded = items => {
  2.   const transform = ol.proj.getTransform('EPSG:4326', 'EPSG:3857');
  3.   items.forEach(item => {
  4.     const feature = new ol.Feature(item);
  5.     feature.set('url', item.preview);
  6.     const coordinate = transform([parseFloat(item.lon), parseFloat(item.lat)]);
  7.     feature.setGeometry(new ol.geom.Point(coordinate));
  8.     photosSource.addFeature(feature);
  9.   });
  10. };
  11. handleMapDataLoaded([{
  12.   "preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
  13.   "lat": 47.765957,
  14.   "lon": 37.255646
  15. }]);
В handleMapDataLoaded пробегаемся по загруженным элементам, создаём объект ol.Feature, преобразуем координаты в проекцию карты и в качестве геометрии выставляем просто точку ol.geom.Point. Затем добавляем объект в photosSource.

Всё, что остаётся сделать, это добавить слой к карте:
  1. const map = new ol.Map({
  2.   target: 'map',
  3.   layers: [layerOSM, layerThumbs],
  4.   ...

Демо

📄 Исходный код


Детали при нажатии
Мелкие фотографии рассматривать не очень удобно, поэтому соберём ещё немного данных о метке. Например, модель камеры, ссылку на оригинал и дату:
  1. handleMapDataLoaded([{
  2.   "name": "dsc_1337_ps_5996.jpg",
  3.   "path": "https://annimon.com/albums/files/dsc_1337_ps_5996.jpg",
  4.   "preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
  5.   "lat": 47.765957,
  6.   "lon": 37.255646,
  7.   "make": "Sony Ericsson",
  8.   "model": "MK16i",
  9.   "date": "2017-06-02 19:30:08"
  10. }]);

При нажатии на фотографию на карте будем показывать детали в отдельной панели:
  1. <div id="map" class="map"></div>
  2. <div id="photo-details"></div>
  3.  
  4. <script type="text/html" id="photo-template">
  5.   <div class="photo-details-container">
  6.     <a href="{path}" target="_blank" title="Click to open photo in new tab">
  7.       <img src="{preview}" class="photo-thumbnail">
  8.     </a>
  9.     <div class="photo-details-content">
  10.       <h3>{name}</h3>
  11.       <p><b>Camera:</b> {make} {model}</p>
  12.       <p><b>Date:</b> {date}</p>
  13.     </div>
  14.   </div>
  15. </script>

При помощи ol.interaction.Select можно добавить обработчики выбора объектов на карте, а также задать им соответствующие стили:
  1. const thumbSelector = new ol.interaction.Select({
  2.   layers: [layerThumbs],
  3.   style: selectedStyle
  4. });
  5. map.addInteraction(thumbSelector);
  6.  
  7. function selectedStyle(feature, resolution) {
  8.   return [photoStyle(feature, clamp(0.4, 0.14 / resolution, 1.2))];
  9. }
Выбор будет происходить на слое layerThumbs. Масштаб выбранной фотографии будет слегка увеличен.
Теперь добавим обработчики выбора и отмены выбора:
  1. const selectedFeatures = thumbSelector.getFeatures();
  2. selectedFeatures.on('add', event => {
  3.   const feature = event.target.item(0);
  4.   const details = photoDetails(feature);
  5.   document.getElementById('photo-details').innerHTML = details;
  6. });
  7. selectedFeatures.on('remove', () => {
  8.   document.getElementById('photo-details').innerHTML = '';
  9. });
  10.  
  11. function photoDetails(feature) {
  12.   let content = document.getElementById('photo-template').innerHTML;
  13.   const keys = ['name', 'preview', 'date', 'lat', 'lon', 'make', 'model', 'path'];
  14.   keys.forEach(key => {
  15.     const value = feature.get(key);
  16.     content = content.replace(`{${key}}`, value);
  17.   });
  18.   return content;
  19. }
При выборе метки в #photo-details вставляем HTML с заменёнными параметрами, перечисленными в массиве const keys = ['name', 'preview', 'date', 'lat', 'lon', 'make', 'model', 'path'];.

Демо

📄 Исходный код

Как видно, если фотографий много, они накладываются друг на друга, поэтому в следующей статье добавим кластеризацию.
  • +4
  • views 92