Кастомизация маркера позиции

В библиотеке MapLibre GL JS позиционный маркер чаще всего ассоциируется с отображением текущего местоположения пользователя на карте. В базовой реализации он может быть представлен через встроенный механизм геолокации или через полностью кастомный DOM-элемент, управляемый вручную.

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


Использование GeolocateControl как базового источника позиции

Стандартный способ отображения позиции основан на контроле:

import maplibregl from "maplibre-gl";

const map = new maplibregl.Map({
  container: "map",
  style: "https://demotiles.maplibre.org/style.json",
  center: [0, 0],
  zoom: 2
});

const geolocate = new maplibregl.GeolocateControl({
  positionOptions: {
    enableHighAccuracy: true
  },
  trackUserLocation: true,
  showAccuracyCircle: true
});

map.addControl(geolocate);

Встроенный контрол создаёт несколько визуальных элементов:

  • маркер текущего положения;
  • круг точности (accuracy circle);
  • индикатор направления (при наличии heading);
  • анимацию обновления позиции.

При этом стилизация через CSS ограничена, поскольку элементы встроены в внутреннюю структуру MapLibre.


Ограничения стандартного позиционного маркера

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

  • ограниченная кастомизация внешнего вида;
  • сложность интеграции фирменного UI;
  • невозможность тонкой настройки анимаций;
  • ограниченный контроль над слоями и взаимодействием;
  • зависимость от внутренней реализации контроля.

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


Кастомный DOM-маркер позиции

Более гибкий подход основан на использовании maplibregl.Marker с HTML-элементом.

const el = document.createElement("div");
el.className = "position-marker";

const marker = new maplibregl.Marker({
  element: el,
  anchor: "center"
})
.setLngLat([30.5, 50.45])
.addTo(map);

CSS-стилизация позволяет полностью управлять визуалом:

.position-marker {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #2e7cff;
  border: 3px solid white;
  box-shadow: 0 0 12px rgba(46, 124, 255, 0.6);
}

Такой подход открывает возможность:

  • создания пульсирующих эффектов;
  • отображения направления движения;
  • динамического изменения цвета;
  • добавления вложенных элементов (иконок, стрелок, аватаров).

Обновление позиции маркера в реальном времени

Для динамического перемещения маркера используется метод setLngLat:

function updatePosition(lng, lat) {
  marker.setLngLat([lng, lat]);
}

Часто источник данных — браузерный API геолокации:

navigator.geolocation.watchPosition((pos) => {
  const { longitude, latitude } = pos.coords;
  updatePosition(longitude, latitude);
});

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

  • throttling обновлений;
  • requestAnimationFrame;
  • игнорирование микродвижений ниже порога точности.

Отображение точности позиции (accuracy circle)

Круг точности не является частью Marker API и реализуется через GeoJSON source и слой circle.

map.addSource("accuracy", {
  type: "geojson",
  data: {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [30.5, 50.45]
    },
    properties: {
      radius: 50
    }
  }
});

Отрисовка слоя:

map.addLayer({
  id: "accuracy-circle",
  type: "circle",
  source: "accuracy",
  paint: {
    "circle-radius": [
      "interpolate",
      ["linear"],
      ["get", "radius"],
      0, 0,
      100, 1000
    ],
    "circle-color": "rgba(46, 124, 255, 0.15)",
    "circle-stroke-color": "rgba(46, 124, 255, 0.4)",
    "circle-stroke-width": 1
  }
});

Обновление данных:

function updateAccuracy(lng, lat, radius) {
  const source = map.getSource("accuracy");
  source.setData({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [lng, lat]
    },
    properties: {
      radius
    }
  });
}

Пульсирующий маркер через CSS-анимацию

Один из распространённых UX-паттернов — пульсация точки текущего положения.

HTML:

const el = document.createElement("div");
el.className = "pulse-marker";

CSS:

.pulse-marker {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #2e7cff;
  position: relative;
}

.pulse-marker::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: rgba(46, 124, 255, 0.4);
  animation: pulse 1.8s infinite;
}

@keyframes pulse {
  0% {
    width: 14px;
    height: 14px;
    opacity: 0.8;
  }
  100% {
    width: 60px;
    height: 60px;
    opacity: 0;
  }
}

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


Отображение направления движения (bearing)

При наличии данных GPS можно визуализировать направление движения через поворот DOM-элемента:

function updateHeading(bearing) {
  el.style.transform = `rotate(${bearing}deg)`;
}

Если используется вложенная структура:

<div class="marker">
  <div class="arrow"></div>
</div>

CSS:

.arrow {
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-bottom: 12px solid #2e7cff;
  transform-origin: center;
}

Синхронизация маркера с камерой карты

В сценариях навигации маркер часто остаётся в центре экрана:

map.setCenter([lng, lat]);

или с плавной анимацией:

map.easeTo({
  center: [lng, lat],
  duration: 500
});

В режиме следования (tracking) важно минимизировать конфликт между пользовательским управлением камерой и автоматическим обновлением позиции.


Использование слоёв для высокопроизводительного позиционного рендеринга

При большом количестве обновлений или сложных визуальных эффектов DOM-маркер может стать узким местом. В таких случаях используется circle или symbol layer.

Пример GeoJSON источника:

map.addSource("user-position", {
  type: "geojson",
  data: {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [30.5, 50.45]
    }
  }
});

Слой:

map.addLayer({
  id: "user-position-layer",
  type: "circle",
  source: "user-position",
  paint: {
    "circle-radius": 8,
    "circle-color": "#2e7cff",
    "circle-stroke-width": 2,
    "circle-stroke-color": "#ffffff"
  }
});

Обновление:

function setPosition(lng, lat) {
  const source = map.getSource("user-position");
  source.setData({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [lng, lat]
    }
  });
}

Комбинированный подход: маркер + слой точности + направление

В реальных интерфейсах позиционный маркер часто состоит из трёх компонентов:

  • центральная точка (Marker или circle layer);
  • радиус точности (circle layer);
  • направление движения (DOM или symbol layer).

Синхронизация всех элементов:

function updateUserState({ lng, lat, accuracy, bearing }) {
  updatePosition(lng, lat);
  updateAccuracy(lng, lat, accuracy);

  if (bearing !== undefined) {
    updateHeading(bearing);
  }

  map.easeTo({
    center: [lng, lat],
    duration: 300
  });
}

Управление частотой обновлений и сглаживание

GPS-данные часто шумные, поэтому применяется сглаживание:

let last = null;

function smoothPosition(newPos) {
  if (!last) {
    last = newPos;
    return newPos;
  }

  const alpha = 0.2;

  const lng = last.lng + (newPos.lng - last.lng) * alpha;
  const lat = last.lat + (newPos.lat - last.lat) * alpha;

  last = { lng, lat };
  return last;
}

Сглаживание уменьшает “дрожание” маркера и улучшает визуальную стабильность.


Работа с масштабированием и якорем маркера

При изменении масштаба карты DOM-маркер остаётся неизменным в пикселях. Для корректного визуального восприятия учитывается:

  • anchor: "center" для симметричного позиционирования;
  • компенсация размеров при zoom;
  • адаптивные стили для плотных экранов.

Пример адаптации:

map.on("zoom", () => {
  const zoom = map.getZoom();
  const scale = Math.min(2, Math.max(0.8, zoom / 10));
  el.style.transform += ` scale(${scale})`;
});

Поведение при потере сигнала геолокации

При отсутствии обновлений позиция может:

  • фиксироваться на последнем значении;
  • отображаться с индикацией “устаревания”;
  • снижать интенсивность визуальных эффектов.

Пример:

function markStale() {
  el.style.opacity = "0.5";
  el.style.filter = "grayscale(1)";
}

Сброс состояния:

function markFresh() {
  el.style.opacity = "1";
  el.style.filter = "none";
}