Рисование точек

В MapLibre GL JS точечные объекты представлены через источники данных GeoJSON и визуализируются с помощью слоёв типа circle или symbol. Такой подход отделяет данные от их отображения: источник описывает геометрию, слой — правила рендеринга.

Точка в GeoJSON задаётся как:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [37.6173, 55.7558]
  },
  "properties": {
    "title": "Москва"
  }
}

Координаты всегда задаются в порядке [долгота, широта], что критично для корректного отображения.


Добавление источника данных (GeoJSON source)

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

map.on('load', () => {
  map.addSource('cities', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [37.6173, 55.7558]
          },
          properties: {
            name: 'Москва',
            population: 11920000
          }
        },
        {
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [30.3351, 59.9343]
          },
          properties: {
            name: 'Санкт-Петербург',
            population: 5384000
          }
        }
      ]
    }
  });
});

Источник может быть как статическим объектом, так и URL, возвращающим GeoJSON.


Отрисовка точек через слой circle

Самый прямой способ визуализации точек — использование слоя circle. Он рисует геометрические круги в местах координат.

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

Параметры слоя позволяют управлять внешним видом:

  • circle-radius — радиус в пикселях
  • circle-color — цвет заливки
  • circle-stroke-width — толщина обводки
  • circle-stroke-color — цвет обводки

Динамическое изменение стиля точек

Стили можно привязывать к свойствам объектов. Например, размер точки может зависеть от числового атрибута:

map.addLayer({
  id: 'cities-scaled',
  type: 'circle',
  source: 'cities',
  paint: {
    'circle-radius': [
      'interpolate',
      ['linear'],
      ['get', 'population'],
      1000000, 4,
      5000000, 8,
      10000000, 14
    ],
    'circle-color': [
      'step',
      ['get', 'population'],
      '#60a5fa',
      5000000,
      '#2563eb',
      10000000,
      '#1e3a8a'
    ]
  }
});

Здесь используются выражения MapLibre:

  • interpolate — плавное изменение значения
  • step — дискретное изменение по порогам
  • get — доступ к свойствам объекта

Использование слоя symbol для точек с текстом

Когда требуется отображать подписи или иконки, применяется слой symbol.

map.addLayer({
  id: 'cities-labels',
  type: 'symbol',
  source: 'cities',
  layout: {
    'text-field': ['get', 'name'],
    'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
    'text-size': 12,
    'text-offset': [0, 1.2],
    'text-anchor': 'top'
  },
  paint: {
    'text-color': '#111827'
  }
});

В этом случае точка становится якорем для текста или изображения.


Комбинирование circle и symbol слоёв

На практике часто используется двойной слой: один отвечает за геометрию, второй — за подписи.

map.addLayer({
  id: 'cities-points',
  type: 'circle',
  source: 'cities',
  paint: {
    'circle-radius': 6,
    'circle-color': '#ef4444'
  }
});

map.addLayer({
  id: 'cities-text',
  type: 'symbol',
  source: 'cities',
  layout: {
    'text-field': ['get', 'name'],
    'text-size': 11,
    'text-offset': [0, 1.5]
  },
  paint: {
    'text-color': '#000000'
  }
});

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


Обновление точек в реальном времени

MapLibre позволяет динамически изменять данные источника:

const source = map.getSource('cities');

source.setData({
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [39.7015, 47.2357]
      },
      properties: {
        name: 'Ростов-на-Дону'
      }
    }
  ]
});

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


Добавление точек по клику

Интерактивное добавление новых точек реализуется через обработчики событий карты:

map.on('click', (e) => {
  const newPoint = {
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [e.lngLat.lng, e.lngLat.lat]
    },
    properties: {
      name: 'Новая точка'
    }
  };

  const source = map.getSource('cities');
  const data = source._data;

  data.features.push(newPoint);
  source.setData(data);
});

Координаты берутся из объекта события lngLat.


Управление кликами по точкам

Точки можно сделать интерактивными, добавив обработчик на слой:

map.on('click', 'cities-circles', (e) => {
  const feature = e.features[0];
  const name = feature.properties.name;

  console.log('Выбрана точка:', name);
});

Для корректной работы необходимо включить queryRenderedFeatures:

map.on('load', () => {
  map.on('click', 'cities-circles', () => {});
});

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

Для улучшения UX часто добавляют изменение стиля при наведении:

map.on('mouseenter', 'cities-circles', () => {
  map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'cities-circles', () => {
  map.getCanvas().style.cursor = '';
});

Также можно менять цвет через состояние feature-state:

map.setFeatureState(
  { source: 'cities', id: 1 },
  { hover: true }
);

И использовать его в стиле:

'circle-color': [
  'case',
  ['boolean', ['feature-state', 'hover'], false],
  '#f59e0b',
  '#3b82f6'
]

Группировка точек (clustering)

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

map.addSource('cities', {
  type: 'geojson',
  data: 'cities.json',
  cluster: true,
  clusterRadius: 40
});

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

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

И слой для отдельных точек:

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

Работа с иконками вместо кругов

Точки могут отображаться как изображения:

map.loadImage('marker.png', (error, image) => {
  map.addImage('custom-marker', image);

  map.addLayer({
    id: 'points-icons',
    type: 'symbol',
    source: 'cities',
    layout: {
      'icon-image': 'custom-marker',
      'icon-size': 0.5,
      'icon-anchor': 'bottom'
    }
  });
});

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


Производительность при большом количестве точек

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

  • использование cluster
  • минимизация свойств в GeoJSON
  • отказ от сложных выражений в paint
  • использование symbol только при необходимости текста
  • предварительная генерализация данных на сервере

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