В настоящее время программистам все чаще приходится сталкиваться с разработкой приложений, взаимодействующих с картами, координатами и маркерами. При разработке такого приложения или модуля необходимо учитывать очень важный фактор: производительность. Представьте себе модуль, который показывает карту и что информация, генерируемая маркерами, растет ежедневно, естественно, если маркеры увеличиваются, с сервера на клиент отправляется больше информации, и может наступить время, когда приложение не отвечает. Решением будет группировка маркеров в кластер для повышения производительности при представлении информации на карте. Это решение не новое, есть библиотеки, такие как Google Maps API, которые решают эту проблему, но они делают это полностью на стороне клиента, запрашивая у сервера всю информацию для группировки маркеров. Это может вызвать проблемы с производительностью на клиенте, так как вся информация должна быть запрошена с сервера для выполнения логики кластеризации.

Во время прочтения этой статьи вы изучите основы группировки маркеров на сервере и отправки их клиенту в минимизированном виде. Можно отметить, что команда разработчиков Codegenio реализовала этот принцип, получив ожидаемый результат.

Зачем кластеризация серверов?

Многие разработчики предпочитают использовать Google Maps API в качестве менеджера карт в своих веб-приложениях. Google Maps API имеет то преимущество, что на его официальном сайте имеется обширная документация. Эта библиотека поддерживает кластеризацию своих маркеров (см. следующий пример).

Это очень хорошо, но что было бы, если бы нам пришлось представлять 100 000 маркеров, или как насчет 500 000 хм, очень вероятно, что браузер не отвечает, потому что он должен сделать запрос на 500 000 записей на сервер, создать на клиенте 500 000 маркеров, а затем сгруппируйте их для отображения на карте. Это, как вы могли заметить, не совсем оптимально, идеальным было бы отправить клиенту как можно меньше информации и представить все данные на карте.

Другими словами, в основном это будет выполнять процесс кластеризации на сервере и отправлять клиенту структуру данных, содержащую по крайней мере одну позицию (широта, долгота) и целое число, представляющее количество маркеров, существующих в заданном радиусе вокруг должность. Наша команда разработчиков недавно столкнулась с этой проблемой и реализовала это решение в приложении Reaquis, в котором его база данных выросла и, следовательно, должна была отображать гораздо больше локаций на карте.

Решение

Если бы нам пришлось назвать зависимость оси для решения проблемы, это, несомненно, была бы библиотека, которая группирует маркеры на сервере, в этом случае в центре внимания будет то, что Codegenio использовал для решения рассматриваемой проблемы. Что ж, магия кластеризации была сделана с библиотекой SuperCluster вместе с форматом обмена GeoJson.

  • GeoJson. Формат для кодирования различных структур географических данных.
  • SuperCluster: очень быстрая библиотека JavaScript для кластеризации геопространственных точек для браузеров и Node.

В основном SuperCluster загружает данные, которые включают координаты (широта, долгота). После загрузки вызывается метод, который группирует все эти данные для заданного уровня масштабирования. Вся эта входная и выходная информация для библиотеки SuperCluster выполняется в формате GeoJSON. Давайте посмотрим на следующий фрагмент кода, который представляет собой не что иное, как логику контроллера SailsJs.

JavaScript

const Supercluster = require('supercluster');
const GeoJSON = require('geojson');
module.exports = {
 clusteredMarkers: async (req, res) => {
  try {
   let zoom = req.param('zoom');
   let data = await getData(req.allParams());
   let geoJsonData = GeoJSON.parse(data, {Point: ['lat', 'lng']});
   let index = new Supercluster({
       radius: 128,
       maxZoom: 18
    }).load(geoJsonData.features);
let clusteredData = index.getClusters([-180, -90, 180, 90], zoom);
res.send({
    success: true,
    total: clusteredData.length,
    records: clusteredData
   });
  } catch (err) {
   if (err) {
    return res.negotiate(err);
   }
  }
 }
};

Вы что-то пропустили? Не волнуйтесь, объясните каждую часть кода.

  • Во-первых, это зависимости SuperCluster и библиотека, которая преобразует любой объект javascript в GeoJSON. Чтобы установить их npm install geojson и npm install supercluster.
  • Переменная zoom получается от клиента, и ее значение изменяется, когда карта изменяется, увеличивается или уменьшается.
  • Переменная data представляет собой массив объектов, получаемых из базы данных. Здесь предполагается, что метод getData() будет обращаться к базе данных и получать данные из коллекции. Если SailsJs используется с MongoDb в качестве базы данных, рекомендуется сделать нативный запрос, ссылку на это можно увидеть здесь. Значение переменной данных должно быть примерно таким:

JSON

[{
   "name": "mark 1",   
   "status": "STAUS-1",
   "street": "81 st"
    ...
   "lat": "55.91581954235805",
   "lng": "39.01556461897831",
},
  { .....
  }
]
  • Переменная _5 — это результат синтаксического анализа объекта данных. Обратите внимание, что второй параметр указывает, какие атрибуты данных содержат широта и долгота для каждого элемента. Результатом будет объект с набором функций, как показано ниже:

JSON

{
 "type": "FeatureCollection":
 "features": [{
    "type": "Feature",
    "geometry": {
     "type": "Point",
     "coordinates": [125.6, 10.1]
      },
    "properties": {
      "name": "mark 1",
      "status": "STAUS-1",
      "street": "81 st",
       ...
      "lat": "55.91581954235805",
      "lng": "39.01556461897831"
      
      }     
     },....
 ]
}
  • index variable является экземпляром SuperCluster и загружается при вызове метода load() путем передачи функций объекта GeoJson.
  • Переменная _7 — это конечный результат группировки, это массив объектов, который будет отправлен клиенту для создания маркеров на карте. В зависимости от информации об объекте будет отображаться кластер или маркер.

JSON

[
 {
 "type": "Feature",
 "id": 2120,
 "properties": {
   "cluster": true,
   "cluster_id": 2120,
   "point_count": 1800,
   "point_count_abbreviated": 1.8k
   },
 "geometry": {
   "type": "Point",
   "coordinates": [
      6.082657800000026,
      50.77498895307875
     ]
  }
},
{
 "type": "Feature",
 "properties": {  
  "name": "mark 1",
  "status": "STAUS-1",
  "street": "81 st"
  },
"geometry": {
    "type": "Point",
    "coordinates": [
       6.0771259,
       50.78158579999999
     ]
  }
},
...
]

Пока что было сделано, это получить данные, которые будут формировать маркеры, преобразовать их в формат GeoJson, загрузить в SuperCluster, кластеризовать и отправить клиенту.

Здесь читателю дается только представление о том, как сгруппировать эти маркеры на сервере и отправить их клиенту, когда на метод приходит HTTP-запрос.

Как мы представляем это в клиенте?

Как упоминалось ранее, API Карт Google является одним из наиболее часто используемых разработчиками, поэтому основное внимание будет сосредоточено на этом API. Наверняка вы заметили, что SuperCluster — это библиотека, разработанная компанией MapBox, но не волнуйтесь, что мы можем заставить ее работать с Google Maps, кроме того, логика работы кластеров сделана на сервере, что позволяет Google Maps не знать, что мы используем Суперкластер.

Для представления маркеров необходимо сделать следующее:

  1. Создать карту
  2. Добавьте событие, которое срабатывает при изменении визуальных границ карты.
  3. Делаем запрос к нашему методу на сервере со значением лимитов и зумом
  4. Представлять маркеры на карте, используя информацию о сервере

JavaScript

var map = new google.maps.Map(document.getElementById('map'));
var markers = [];
map.addListener('bounds_changed', function () {
 var bounds = map.getBounds();
let northEast = bounds.getNorthEast();
 let southWest = bounds.getSouthWest();
 var zoom = map.zoom;
 getDataFromServer(southWest.lng(), southWest.lat(), northEast.lng(), northEast.lat(),
  zoom).then(function (res) {
if (markers.length !== []) {
   markers.forEach(function (marker) {
    marker.setMap(null);
   });
  }
  var geoJsonData = res.records;
  for (var data of geoJsonData) {
   if (data.properties.cluster) {
    markers.push(new google.maps.Marker({
      label: data.properties.point_count_abbreviated,
      icon: {
       path: google.maps.SymbolPath.CIRCLE        
      },
      map: map
     })
    );
} else {
    markers.push(new google.maps.Marker({
      label: data.properties.name,
      icon: {
       path: google.maps.SymbolPath.BACKWARD_OPEN_ARROW
      },
      map: map
     })
    );
   }
  }
 });
});

В этом коде рассматриваются эти 4 аспекта. Это базовый пример того, что конечным результатом будет отображение сгруппированных маркеров, которые были сгруппированы на сервере.

Если вы знакомы с Google Maps API, вам не составит труда понять работу предыдущего кода. Обратите внимание, предполагается, что метод getDataFromServer() отвечает за переход на сервер через HTTP-запрос к методу clusteredMarkers нашего сервера и получение информации в формате GeoJson для построения маркеров. Этот метод не был реализован, потому что это не является целью этой статьи, но вы можете использовать некоторые из библиотек, используемых для выполнения HTTP-запросов, таких как Request, axios или socket.io.

Выводы

Если вы дошли до этого момента, мы можем поделиться выводом этой статьи, акцентируя внимание на том, что выполнение процесса группировки маркеров на сервере гарантирует, что наше приложение не рухнет при уменьшении масштаба карты и нескольких тысяч маркеров. должно отображаться. То, что показано здесь, является лишь примером, который разработчики могут использовать для применения этого принципа в модулях, которые взаимодействуют с картами, требующими отображения больших объемов информации.

Эта статья была изначально опубликована в Блоге Codegenio нашим бэкэнд-инженером Дариэлем Ноа.