image

Технологии

Выжать максимум
из Leaflet: создаём интерактивную карту на 200К объектов

Кто-то прямо сейчас спросит: а зачем это всё, если есть готовые решения? И правда есть. Для нашего случая цена вопроса около ₽500 тысяч. А уверенности, что ГИС из коробки решит все задачи, – никакой. Поэтому здесь будет история про альтернативный путь, веру в силу мысли и бесконечных возможностей бесплатных библиотек.

image

Осознаем масштабы

Итак, мы создаём веб-приложение на React для кадастрового учёта с детальным отображением 200 000 объектов и их границ на карте.

По дороге:

1не теряем производительность, чтобы всё было плавно и глаз не дёргался;

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

И ещё одна деталь. Раньше команда не работала с картами. На старте мы знакомимся со всем «ассортиментом» технологий и уже начинаем подозревать, что простого решения не будет. Так и оказалось.

Поехали!

За основу взяли одну из самых популярных опенсорсных библиотек Leaflet и её версию для React.

icon_qute

React Leaflet обеспечивает привязку между React и Leaflet. Он не заменяет Leaflet, но использует его для абстрагирования слоёв Leaflet как компонентов React.

Начинаем работать с react-leaflet и обнаруживаем, что недавно выкатили стабильную третью версию. Многие кастомные плагины здесь уже не работают, а значит переписывать их придётся самим. Это время. Но есть подозрения, что новая версия лучше старой. Поэтому мы продолжаем.

...И приехали

Разобравшись с новыми плагинами, выгружаем в react-leaflet первые порции данных.

Обычно в примерах библиотек максимальный зум, где все красиво и плавно, – это страны мира. А когда хочешь отобразить несколько тысяч участков или домов, всё уже не так радужно.

И что получается у нас? На тысячах всё летает, пробуем 50 тысяч – лежит. Вспоминаем о своих 200 тысячах и идем копать дальше.

import { MapContainer, TileLayer, GeoJSON } from "react-leaflet"; import geoJSONData from "./geoJSON"; import "leaflet/dist/leaflet.css"; import "./styles.css"; export default function App() { const position = [55.706415, 37.426097]; return ( <MapContainer className="map-container" center={position} zoom={16}> <TileLayer attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <GeoJSON data={geoJSONData} /> // В data грузим весь объем данных </MapContainer> ); }

Находим проблему в отображении в svg. Они забивают DOM-дерево и тормозят браузер. После ресёрча решаем уходить на canvas, который как раз придумали для эффективной отрисовки графики в браузере. Здесь процесс вычисления переносится на GPU. А мы выдыхаем: скорость отображения выросла и устраивает нас.

<MapContainer className="map-container" preferCanvas // рендерит объекты с помощью <canvas> center={position} zoom={16} > <TileLayer attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <GeoJSON data={geoJSONData} /> </MapContainer>

Но получать 200 тысяч объектов одним куском – всё равно идея не очень, потому что весит он ~ 60 Мб.

Упрощаем границы

Для передачи географических структур мы остановились на GeoJSON.

Формат для нас мог быть идеальным, если бы не слишком много кода для одного объекта.

Структура geoJSON

{ "type": "FeatureCollection", "features": [ { // Данные одного объекта "type": "Feature", "properties": { "shape": "Polygon", "name": "My Layer", "category": "default" }, "geometry": { "type": "Polygon", "coordinates": [ [ [37.425533, 55.70643], [37.425877, 55.706345], [37.425957, 55.706445], [37.425635, 55.706535], [37.425533, 55.70643] ] ] }, "id": "243256" }, {...}, {...}, ] }

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

А мы снова ищем другой путь.

Пробуем Bounding Box

Дословно это “ограничивающая рамка” (обычно сокращается до bbox). Если просто – это границы области видимости карты. С этим подходом мы грузим и отображаем только те данные, которые попадают в область видимости.

В момент перемещения отлавливаем границы карты.

map.getBounds();

Как это работает?

icon_qute

На бэк отправляем координаты крайних точек области видимости и обратно получаем данные по этим координатам.

Часть проблемы мы решили. Но дальше 15-го зума в область видимости попадает уже много объектов, а их передача занимает всё больше времени. А это значит, что мы продолжаем поиск альтернативных вариантов…

И находим Vector Tiles

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

Pbf vector tile отображаем с помощью плагина Leaflet Vector Grid.

По умолчанию он выдаёт svg, но мы переходим на сanvas, с которым решили работать раньше. И (ура!) скорость работы клиент-сервер с этим решением нас устраивает.

export default function App() { const position = [55.706415, 37.426097]; const options = { type: 'protobuf', rendererFactory: L.canvas.tile, // рендерит тайлы с помощью <canvas> url: 'https://free-{s}.tilehosting.com/data/v3/{z}/{x}/{y}.pbf.pict?key={key}' vectorTileLayerStyles: { ... }, subdomains: 'abcd', key: 'abcdefghi01234567890' }; return ( <MapContainer className="map-container" preferCanvas center={position} zoom={16} > <TileLayer attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <VectorGrid {...options} /> <GeoJSON data={geoJSONData} /> </MapContainer> ); }

Добавляем объекты

У PBF Vector Grid появились проблемы с обработкой событий. Клики по нужному объекту не распознавались или распознавались на слое, который наложился последним.

Слоёв у нас много, а объекты находятся внутри других: внешних границ кварталов, земельных участков.

Наши ключевые задачи – дать привязаться к существующим границам, стилизовать объекты и обработать события. PBF Vector Grid слой не даёт нам реализовать их полностью. Поэтому продолжаем работать с GeoJSON и bBox.

Для добавления новых объектов с привязкой к существующим границам используем бесплатную версию библиотеки leaflet geoman.

У неё есть фича «примагничивать» точки к существующим границам объектов. Если зажать Alt, примагничивание отключается.

export default function Geoman() { const map = useMap(); map.pm.addControls(); map.pm.setGlobalOptions({ snapDistance: 15, // Устанавливаем расстояние примагничивания }); return null; }

Так мы добились точного позиционирования курсора и 100% совмещения границ при отрисовке смежных объектов.

Пожалели мы, что не взяли готовую ГИС и отдали время на всё описанное выше? Нет. Если, как и Leaflet, ГИС из коробки не могут закрыть задачу, то почему бы не использовать библиотеки с открытым исходным кодом. Даже без бэкграунда работы с картами на форумах и в открытых источниках можно найти решения или хотя бы зацепки. А не тратить на старте кучу денег без гарантии успеха.

Небольшая сборка на сандбоксе.

Читайте далее...

image
Технологии

Три неудобные ситуации, от которых дополненная реальность избавляет сервисную службу

Изучили исследования и смоделировали кейсы из ближайшего будущего послепродажного обслуживания техники.

Подробнее icon_arrow_right
image
Обучение

Курс или тренинг? Гайд по форматам и направлениям обучения в Clevertec

Когда новый набор? А тестовое будет? Отвечаем на вопросы, чтобы вам было проще ориентироваться в образовательных возможностях компании.

Подробнее icon_arrow_right