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

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

Геоданные как основа отображения

Любые результаты пространственного анализа или запроса приводятся к формату GeoJSON. Это универсальное представление, которое поддерживает:

  • точки (Point)
  • линии (LineString)
  • полигоны (Polygon)
  • коллекции объектов (FeatureCollection)

Структура результата определяет дальнейшую стратегию визуализации. Например, одиночные точки часто отображаются как маркеры, линии — как маршруты или границы, полигоны — как области или зоны влияния.

Ключевым моментом является то, что MapLibre GL JS не работает напрямую с “результатами запросов” как абстракцией. Каждый результат должен быть преобразован в источник данных:

map.addSource('results', {
  type: 'geojson',
  data: geojsonData
});

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


Базовое отображение результатов через слои

Слои определяют, как именно геометрия будет визуализирована. Для результатов чаще всего используются три типа слоёв:

  • circle — для точек
  • line — для линейных объектов
  • fill — для полигонов

Простейшее отображение точечных результатов:

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

Для линейных результатов:

map.addLayer({
  id: 'results-lines',
  type: 'line',
  source: 'results',
  paint: {
    'line-color': '#3f51b5',
    'line-width': 3
  }
});

Для полигональных результатов:

map.addLayer({
  id: 'results-polygons',
  type: 'fill',
  source: 'results',
  paint: {
    'fill-color': '#4caf50',
    'fill-opacity': 0.4
  }
});

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


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

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

const source = map.getSource('results');
source.setData(newGeojsonData);

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

Динамическое обновление особенно важно при:

  • фильтрации данных
  • поиске объектов
  • построении маршрутов
  • интерактивной геообработке

Визуальная дифференциация результатов

При отображении множества результатов возникает необходимость их различения. MapLibre GL JS предоставляет несколько стратегий стилизации на основе свойств объектов.

Условная стилизация

map.addLayer({
  id: 'results-conditional',
  type: 'circle',
  source: 'results',
  paint: {
    'circle-radius': 6,
    'circle-color': [
      'match',
      ['get', 'type'],
      'hospital', '#e91e63',
      'school', '#2196f3',
      'park', '#4caf50',
      '#9e9e9e'
    ]
  }
});

Интерполяция значений

map.addLayer({
  id: 'results-intensity',
  type: 'circle',
  source: 'results',
  paint: {
    'circle-radius': [
      'interpolate',
      ['linear'],
      ['get', 'score'],
      0, 4,
      10, 10,
      20, 18
    ],
    'circle-color': '#ff9800'
  }
});

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


Фильтрация отображаемых результатов

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

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

Более сложные выражения позволяют комбинировать условия:

map.setFilter('results-points', [
  'all',
  ['==', ['get', 'active'], true],
  ['>=', ['get', 'rating'], 4]
]);

Фильтрация выполняется на GPU, что делает её эффективной даже при больших наборах данных.


Интерактивное выделение объектов

Для работы с результатами в интерактивном режиме используется feature state. Этот механизм позволяет изменять визуальное состояние отдельных объектов без изменения GeoJSON.

map.setFeatureState(
  { source: 'results', id: featureId },
  { selected: true }
);

Стилизация слоя:

map.addLayer({
  id: 'results-highlight',
  type: 'circle',
  source: 'results',
  paint: {
    'circle-radius': [
      'case',
      ['boolean', ['feature-state', 'selected'], false],
      10,
      6
    ],
    'circle-color': [
      'case',
      ['boolean', ['feature-state', 'selected'], false],
      '#ff0000',
      '#00897b'
    ]
  }
});

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


Работа с запросами по видимой области карты

Часто результаты формируются на основе текущего экстента карты. Для этого используется:

const features = map.queryRenderedFeatures({
  layers: ['results-points']
});

Или с ограничением по области:

const bboxFeatures = map.queryRenderedFeatures({
  geometry: map.getBounds().toArray()
});

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


Кластеризация результатов

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

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

Отдельные слои для кластеров и одиночных точек:

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

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

Кластеризация позволяет сохранить читаемость карты при высокой плотности результатов.


Отображение плотности через heatmap

Для непрерывных распределений применяется тепловая карта:

map.addLayer({
  id: 'results-heat',
  type: 'heatmap',
  source: 'results',
  paint: {
    'heatmap-intensity': 1,
    'heatmap-radius': 20,
    'heatmap-color': [
      'interpolate',
      ['linear'],
      ['heatmap-density'],
      0, 'rgba(0,0,255,0)',
      0.5, 'rgba(0,255,0,0.5)',
      1, 'rgba(255,0,0,0.8)'
    ]
  }
});

Heatmap применяется к большим наборам точек, где важно показать общую структуру распределения, а не отдельные элементы.


Всплывающие окна для результатов

Информационные всплывающие окна связываются с конкретными объектами слоя:

map.on('click', 'results-points', (e) => {
  const coordinates = e.features[0].geometry.coordinates.slice();
  const properties = e.features[0].properties;

  new maplibregl.Popup()
    .setLngLat(coordinates)
    .setHTML(`
      <div>
        <strong>${properties.name}</strong><br/>
        ${properties.description}
      </div>
    `)
    .addTo(map);
});

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


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

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

  • точки поиска
  • маршруты
  • области анализа
  • агрегированные зоны

Согласованность достигается через разделение источников:

map.addSource('search-results', { type: 'geojson', data: searchData });
map.addSource('routes', { type: 'geojson', data: routeData });
map.addSource('zones', { type: 'geojson', data: zoneData });

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


Приоритизация и порядок слоёв

Порядок добавления слоёв определяет визуальную иерархию результатов. Более важные элементы размещаются выше:

map.addLayer(layerConfig, 'waterway-label');

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


Производительность отображения результатов

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

  • минимизация количества источников
  • использование кластеризации
  • применение фильтров вместо пересоздания данных
  • ограничение числа активных слоёв
  • использование feature-state вместо модификации GeoJSON

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


Обновление визуального состояния без перезагрузки данных

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

  • данные хранятся в источнике GeoJSON
  • состояние хранится в feature-state
  • визуализация определяется стилем слоя

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