Кто-то прямо сейчас спросит: а зачем это все, если есть готовые решения? И правда есть. Для нашего случая цена вопроса около ₽500 тысяч. А уверенности, что ГИС из коробки решит все задачи, – никакой. Поэтому здесь будет история про альтернативный путь, веру в силу мысли и бесконечных возможностей бесплатных библиотек.
Осознаем масштабы
Итак, мы создаем веб-приложение на React для кадастрового учета с детальным отображением 200 000 объектов и их границ на карте.
- не теряем производительность, чтобы чтобы все было плавно и глаз не дергался;
- сохраняем интерактивность: нужно выделять объекты, отображать информацию о выбранном объекте, вносить изменения и добавлять новые объекты с привязкой к существующим границам, включать и выключать слои. В общем, полный фарш на фронте.
По дороге:
И еще одна деталь. Раньше команда не работала с картами. На старте мы знакомимся со всем «ассортиментом» технологий и уже начинаем подозревать, что простого решения не будет. Так и оказалось.
Поехали!
За основу взяли одну из самых популярных опенсорсных библиотек Leaflet и ее версию для React.
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='© <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='© <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();
Как это работает?
На бэк отправляем координаты крайних точек области видимости и обратно получаем данные по этим координатам.
Часть проблемы мы решили. Но дальше 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='© <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, ГИС из коробки не могут закрыть задачу, то почему бы не использовать библиотеки с открытым исходным кодом. Даже без бэкграунда работы с картами на форумах и в открытых источниках можно найти решения или хотя бы зацепки. А не тратить на старте кучу денег без гарантии успеха.
P. S. Небольшая сборка на сандбоксе.