Отображение результатов на карте

Работа с результатами пространственных данных в MapLibre GL JS строится вокруг связки источников данных (sources), слоёв (layers) и механизмов динамического обновления состояния карты. Архитектура ориентирована на декларативное описание визуализации, при котором данные отделены от их отображения, а рендеринг выполняется через GPU.


Источники данных как основа визуализации результатов

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

map.addSource('results', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: []
  }
});

Источник выступает контейнером, который может динамически обновляться без пересоздания слоя. При изменении данных применяется метод:

map.getSource('results').setData(newGeojson);

Такой подход позволяет отделить бизнес-логику получения результатов (поиск, фильтрация, агрегация) от визуализации.


Отрисовка точечных результатов через слои

Для отображения результатов поиска или аналитики чаще всего используется слой circle или symbol.

map.addLayer({
  id: 'results-points',
  type: 'circle',
  source: 'results',
  paint: {
    'circle-radius': 6,
    'circle-color': '#3b82f6',
    'circle-stroke-width': 1,
    'circle-stroke-color': '#ffffff'
  }
});

Каждый объект GeoJSON преобразуется в отдельный визуальный элемент. При необходимости можно использовать стилизацию на основе свойств:

'circle-color': [
  'match',
  ['get', 'category'],
  'restaurant', '#ef4444',
  'hotel', '#3b82f6',
  '#10b981'
]

Такой механизм позволяет визуально разделять типы результатов без изменения структуры данных.


Отображение кластеризованных результатов

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

map.addSource('results', {
  type: 'geojson',
  data: geojson,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

Отображение кластеров реализуется через отдельные слои:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'results',
  filter: ['has', 'point_count'],
  paint: {
    'circle-color': '#2563eb',
    'circle-radius': 18
  }
});

map.addLayer({
  id: 'cluster-count',
  type: 'symbol',
  source: 'results',
  filter: ['has', 'point_count'],
  layout: {
    'text-field': '{point_count_abbreviated}',
    'text-size': 12
  }
});

Отдельный слой используется для отображения единичных объектов:

map.addLayer({
  id: 'unclustered-point',
  type: 'circle',
  source: 'results',
  filter: ['!', ['has', 'point_count']],
  paint: {
    'circle-color': '#f59e0b',
    'circle-radius': 6
  }
});

Визуализация результатов поиска и фильтрации

Результаты пользовательских запросов обычно формируются динамически. После получения данных выполняется преобразование в GeoJSON и обновление источника.

function updateResults(items) {
  const geojson = {
    type: 'FeatureCollection',
    features: items.map(item => ({
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [item.lon, item.lat]
      },
      properties: {
        id: item.id,
        title: item.title,
        type: item.type
      }
    }))
  };

  map.getSource('results').setData(geojson);
}

Фильтрация может выполняться как на уровне данных, так и через выражения слоя:

map.setFilter('results-points', [
  '==',
  ['get', 'type'],
  'restaurant'
]);

Такой подход уменьшает необходимость перегенерации GeoJSON при каждом изменении UI-фильтров.


Интерактивное взаимодействие с результатами

Для обработки кликов используется механизм событий карты:

map.on('click', 'results-points', (e) => {
  const feature = e.features[0];
});

Полученные свойства объекта позволяют формировать динамические интерфейсы, такие как всплывающие окна:

new maplibregl.Popup()
  .setLngLat(feature.geometry.coordinates)
  .setHTML(`<div>${feature.properties.title}</div>`)
  .addTo(map);

При необходимости можно реализовать выделение активного объекта через изменение состояния слоя:

map.setPaintProperty('results-points', 'circle-radius', [
  'case',
  ['==', ['get', 'id'], activeId],
  10,
  6
]);

Автоматическое масштабирование к результатам

После обновления данных часто выполняется подгонка области видимости карты:

const bounds = new maplibregl.LngLatBounds();

geojson.features.forEach(f => {
  bounds.extend(f.geometry.coordinates);
});

map.fitBounds(bounds, {
  padding: 50
});

Такой механизм обеспечивает визуальную фокусировку на текущем наборе результатов.


Работа с векторными слоями результатов

При использовании векторных тайлов результаты могут поступать не в формате GeoJSON, а как тайловые слои сервера. В этом случае фильтрация и стилизация выполняются через source-layer:

map.addLayer({
  id: 'vector-results',
  type: 'fill',
  source: 'tiles',
  'source-layer': 'results_layer',
  paint: {
    'fill-color': '#60a5fa',
    'fill-opacity': 0.5
  }
});

Такой подход снижает нагрузку на клиентскую сторону и переносит обработку на сервер генерации тайлов.


Динамическое обновление состояния результатов

Состояние визуализации может изменяться без перезагрузки источника. Используются методы:

  • setData — полная замена данных
  • setFilter — фильтрация слоя
  • setPaintProperty — изменение визуального стиля
  • setLayoutProperty — изменение структуры отображения

Пример переключения режима отображения:

function toggleView(mode) {
  if (mode === 'heatmap') {
    map.setLayoutProperty('results-points', 'visibility', 'none');
  } else {
    map.setLayoutProperty('results-points', 'visibility', 'visible');
  }
}

Использование выражений для контекстного отображения

Выражения MapLibre позволяют создавать контекстно-зависимую визуализацию результатов:

'circle-opacity': [
  'interpolate',
  ['linear'],
  ['zoom'],
  5, 0.2,
  10, 0.8
]

Также возможно комбинирование свойств объекта:

'circle-stroke-width': [
  '+',
  ['get', 'importance'],
  1
]

Такая модель обеспечивает гибкую адаптацию визуализации под масштаб и данные.


Оптимизация отображения большого количества результатов

При работе с тысячами объектов критическим становится снижение нагрузки на рендеринг:

  • использование кластеризации вместо отдельных маркеров
  • минимизация количества слоёв
  • перенос агрегации на сервер
  • использование vector tiles вместо GeoJSON
  • ограничение перерисовки через батчевое обновление setData

Дополнительно применяется фильтрация по зуму:

'filter': ['>=', ['zoom'], 8]

Синхронизация результатов с внешним состоянием приложения

В интерфейсах с панелями поиска и фильтрации карта выступает как визуальный слой состояния. Любое изменение UI должно транслироваться в обновление источника данных или фильтров слоёв.

Пример синхронизации:

searchResults.subscribe(items => {
  map.getSource('results').setData(toGeoJSON(items));
});

Такой подход обеспечивает реактивное обновление визуализации без прямой привязки к DOM.


Комбинирование различных типов представления результатов

Одновременное использование разных слоёв позволяет создавать многослойную визуализацию:

  • circle — точки результатов
  • symbol — подписи
  • heatmap — плотность распределения
  • fill — области покрытия

Пример heatmap-слоя:

map.addLayer({
  id: 'heatmap',
  type: 'heatmap',
  source: 'results',
  paint: {
    'heatmap-intensity': 1,
    'heatmap-radius': 20
  }
});

Комбинация слоёв позволяет одновременно отображать детали и агрегированную картину распределения данных.