Метки на картах это просто. Кластеризация

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

Если фотографий много, то отображать их все сразу на карте — не самая лучшая идея, как в плане UX, так и в плане производительности. Поэтому давайте сгруппируем фотографии и покажем их количество.
ologo2.jpg

Для группировки в OpenLayers есть кластер ol.source.Cluster. Из коробки он работает с точками (ol.geom.Point), так что для нашей задачи его будет несложно добавить. Задаём ему дистанцию и откуда брать точки (photosSource), а также переименуем layerThumbs в layerClusters:
  1. const clusterSource = new ol.source.Cluster({
  2.   distance: 30,
  3.   minDistance: 10,
  4.   source: photosSource
  5. });
  6. const layerClusters = new ol.layer.Vector({
  7.   source: clusterSource,
  8.   style: clusterStyle,
  9.   updateWhileAnimating: true,
  10.   updateWhileInteracting: true,
  11. });
  12.  
  13. const map = new ol.Map({
  14.   // ..
  15.   layers: [layerOSM, layerClusters],
  16.   // ..
  17. });
  18.  
  19. function clusterStyle(feature, resolution) {
  20.   const features = feature.get('features');
  21.   const size = features.length;
  22.   const key = `cl-${size}`;
  23.   if (!cache[key]) {
  24.     cache[key] = new ol.style.Style({
  25.       zIndex: 110,
  26.       image: new ol.style.Circle({
  27.         radius: 12,
  28.         stroke: new ol.style.Stroke({color: '#8AFFD9'}),
  29.         fill: new ol.style.Fill({color: '#229D75'})
  30.       }),
  31.       text: new ol.style.Text({
  32.         text: `${size}`,
  33.         fill: new ol.style.Fill({color: '#fff'})
  34.       }),
  35.     });
  36.   }
  37.   return cache[key];
  38. }
clusterStyle похож на photoStyle, только рисует не иконки, а окружность с заливкой и обводкой, а поверх неё текст.

Так как отдельных фотографий теперь на карте нет, то выбирать можно только эти кружочки — придётся переписать функцию выбора:
  1. map.on('click', event => {
  2.   layerClusters.getFeatures(event.pixel).then(clickedFeatures => {
  3.     if (!clickedFeatures.length) return;
  4.     const features = clickedFeatures[0].get('features');
  5.     const details = features.map(f => photoDetails(f));
  6.     document.getElementById('photo-details').innerHTML = details.join();
  7.   });
  8. });
При клике на карту собираем массив объектов, попадающих в область выбора и показываем детали уже для них.

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

Можно показать и отдельные фотографии, если объект в кластере только один:
  1. function clusterStyle(feature, resolution) {
  2.   const features = feature.get('features');
  3.   const size = features.length;
  4.   if (size === 1) return thumbStyle(features[0], resolution);
  5.   // ..


Можно усложнить логику выбора, добавив плавное приближение к фотографиям при нажатии на кластер:
  1. map.on('click', event => {
  2.   layerClusters.getFeatures(event.pixel).then(clickedFeatures => {
  3.     if (!clickedFeatures.length) return;
  4.     const features = clickedFeatures[0].get('features');
  5.     if (features.length > 1 && !isMaximumZoom(map.getView())) {
  6.       // Zoom in
  7.       const extent = ol.extent.boundingExtent(
  8.         features.map(r => r.getGeometry().getCoordinates()),
  9.       );
  10.       map.getView().fit(extent, {duration: 400, padding: [150, 150, 150, 150]});
  11.     } else {
  12.       // Show photo details
  13.       const details = features.map(f => photoDetails(f));
  14.       document.getElementById('photo-details').innerHTML = details.join();
  15.     }
  16.   });
  17. });
  18.  
  19. function isMaximumZoom(view) {
  20.   const maxZoom = view.getMaxZoom();
  21.   const currentZoom = view.getZoom();
  22.   return currentZoom >= maxZoom;
  23. }
Если выбрали круг с 2+ элементами и при этом есть ещё куда увеличивать карту, то перебираем все координаты в выбранном кластере и формируем из них виртуальную область extent:
  1. const extent = ol.extent.boundingExtent(
  2.   features.map(r => r.getGeometry().getCoordinates()),
  3. );
затем в течение 400мс приближаем карту к этой области так, чтобы остались отступы по краям в 150 пикселей:
  1. map.getView().fit(extent, {duration: 400, padding: [150, 150, 150, 150]});;

Демо:
📄 Исходный код
  • +1
  • views 23