Маркеры SVG в Leaflet: совместите кончик с iconAnchor
Размещайте собственные SVG-маркеры в Leaflet, сопоставляя видимый кончик с iconAnchor. Учитывайте поля viewBox, вписывание с сохранением пропорций и изменение размера значка.
На этой странице
Для собственного SVG-маркера Leaflet задайте iconSize равным размерам показываемого изображения, а iconAnchor — пиксельной позиции видимого кончика внутри него. Измеряйте от верхнего левого угла изображения. Прозрачные поля SVG входят в эти размеры, поэтому половина ширины и полная высота могут не попасть в кончик асимметричной метки или метки с полями.
В руководстве используется оригинальная метка, чей кончик находится справа от центра её тела. Мы сравниваем две исходные рамки в двух размерах значка на браузерной карте без тайлов. Если чистая SVG-графика уже есть, используйте её напрямую: размещению якоря векторизация не нужна.
Отделите графику, прямоугольник изображения и точку карты
Здесь встречаются три системы координат. Контур SVG описывает графику в исходных единицах. iconSize задаёт её показываемый прямоугольник в пикселях. Leaflet совмещает точку внутри этого прямоугольника, заданную iconAnchor, с географическим положением маркера. Справочник Icon определяет эти параметры и начало отсчёта якоря в верхнем левом углу.
Обычная метка может доходить до середины нижнего края, но ваша графика может заканчиваться в другом месте. Описанное в документации значение Leaflet по умолчанию — центральный якорь, если размер указан. Значения нижнего центра в нашем эксперименте — явно проверяемое допущение, а не это значение по умолчанию.
Официальный урок по собственным значкам показывает передачу экземпляра L.icon маркеру. Для импортированной графики дополнительно нужно найти точку внутри изображения, которая должна касаться координаты карты.

Переведите исходный кончик в пиксели значка
Кончик нашей метки находится в исходной координате (40, 80). Плотная рамка — viewBox="0 0 60 80". Вариант с полями меняет только рамку на viewBox="-20 -10 100 120"; оба файла сохраняют одинаковый контур и прозрачное отверстие.
Справочник viewBox описывает этот исходный прямоугольник. Пустое пространство внутри него перемещается вместе с графикой. Наше руководство по ограничивающему прямоугольнику SVG объясняет, почему у холста и закрашенной фигуры могут быть разные границы.
Обе версии явно используют preserveAspectRatio="xMidYMid meet". Он равномерно вписывает рамку в область просмотра и центрирует оставшееся пространство. Эти настройки описаны в справочнике MDN по preserveAspectRatio.
Для этого режима вписывания пусть W и H — размеры значка, (minX, minY, vbW, vbH) — viewBox, а (tipX, tipY) — исходный кончик:
s = min(W / vbW, H / vbH)
offsetX = (W - vbW * s) / 2
offsetY = (H - vbH * s) / 2
anchorX = (tipX - minX) * s + offsetX
anchorY = (tipY - minY) * s + offsetYДля значка с полями 60×80 s равно 0.6. Исходная рамка 100×120 занимает 60×72 пикселя, оставляя по четыре пикселя сверху и снизу. Кончик становится [36, 58] с учётом этого вертикального смещения. Простое вычисление доли каждого размера по отдельности пропустит этап равномерного вписывания.
| Исходная рамка | Размер значка | Рассчитанный якорь кончика |
|---|---|---|
| Плотная, 0 0 60 80 | 60×80 | [40, 80] |
| Плотная, 0 0 60 80 | 90×120 | [60, 120] |
| С полями, -20 -10 100 120 | 60×80 | [36, 58] |
| С полями, -20 -10 100 120 | 90×120 | [54, 87] |
Эти расчёты относятся к нашему изображению без преобразований и явному центральному meet. Другое выравнивание, slice или none требует соответствующего преобразования координат. Наше руководство по пропорциям описывает эти варианты. Дополнительные преобразования CSS изображения маркера тоже нуждаются в отдельной проверке.
Проверьте размещение на карте без тайлов
Мы использовали Leaflet 1.9.4 — стабильный выпуск, указанный на официальной странице загрузки. Эксперимент локально загружает JavaScript и CSS закреплённой версии и не добавляет тайловый слой. Каждая карта проецирует координату [0, 0] на синее перекрестие и показывает одно SVG-изображение рядом с этой опорной точкой.
Проверка исходников нашла один одинаковый составной контур и отсутствие встроенного растра в каждом SVG. Размеры изображения в браузере были 60×80 и 90×120 пикселей, как запрошено. Используя наблюдаемый прямоугольник каждого изображения, мы сравнили рассчитанную позицию кончика с проецируемой географической точкой:
| Исходник | Размер значка | Ошибка при предполагаемом якоре внизу по центру | Ошибка с рассчитанным якорем кончика |
|---|---|---|---|
| Плотный | 60×80 | 10 px вправо | 0 px |
| Плотный | 90×120 | 15 px вправо | 0 px |
| С полями | 60×80 | 6 px вправо, 22 px вверх | 0 px |
| С полями | 90×120 | 9 px вправо, 33 px вверх | 0 px |
Перекрестие также визуально совпало с исправленными кончиками меток. Это наблюдения нашего тестового примера в одном Chrome, а не гарантия для любого SVG, браузера или интеграции карты. Метка с полями после исправления якоря остаётся меньше внутри того же прямоугольника изображения. Размещение и размер графики — отдельные решения.
Скопируйте рабочий стартовый пример маркера
Сохраните этот полный HTML как marker.html и раздавайте локальным веб-сервером. Он загружает закреплённые файлы Leaflet из CDN, на который ссылается страница загрузки Leaflet. SVG закодирован в URL изображения, поэтому примеру не нужен файл значка или сервис тайлов. Для двух файлов библиотеки нужен доступ в интернет.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Leaflet SVG tip anchor</title>
<link rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
#map { width: 360px; height: 300px; background: #edf3ef; }
.cross { position: absolute; z-index: 1000; pointer-events: none;
width: 24px; height: 24px; transform: translate(-50%, -50%); }
.cross::before, .cross::after { content: ""; position: absolute;
background: #006cbe; }
.cross::before { top: 12px; width: 24px; height: 1px; }
.cross::after { left: 12px; width: 1px; height: 24px; }
</style>
<div id="map"></div>
<pre id="status">Loading marker…</pre>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="60" height="80" viewBox="-20 -10 100 120" preserveAspectRatio="xMidYMid meet"><path fill="#bf593c" fill-rule="evenodd" d="M30 0 C46 0 60 12 60 28 C60 46 46 54 40 80 C30 62 0 46 0 28 C0 12 14 0 30 0 Z M40 26 A10 10 0 1 0 20 26 A10 10 0 1 0 40 26 Z"/></svg>`;
const size = [60, 80];
const viewBox = [-20, -10, 100, 120];
const sourceTip = [40, 80];
const scale = Math.min(size[0] / viewBox[2], size[1] / viewBox[3]);
const anchor = [
(sourceTip[0] - viewBox[0]) * scale
+ (size[0] - viewBox[2] * scale) / 2,
(sourceTip[1] - viewBox[1]) * scale
+ (size[1] - viewBox[3] * scale) / 2
];
const map = L.map("map", {
zoomControl: false, attributionControl: false,
dragging: false, scrollWheelZoom: false, doubleClickZoom: false,
boxZoom: false, keyboard: false, touchZoom: false
}).setView([0, 0], 2);
const position = [0, 0];
const icon = L.icon({
iconUrl: "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg),
iconSize: size,
iconAnchor: anchor
});
const marker = L.marker(position, { icon }).addTo(map);
const container = document.querySelector("#map");
const cross = document.createElement("span");
cross.className = "cross";
container.append(cross);
const point = map.latLngToContainerPoint(position);
cross.style.left = point.x + "px";
cross.style.top = point.y + "px";
(async () => {
const image = marker.getElement();
await image.decode();
const ir = image.getBoundingClientRect();
const mr = container.getBoundingClientRect();
const displacement = [
ir.left + anchor[0] - mr.left - point.x,
ir.top + anchor[1] - mr.top - point.y
];
document.querySelector("#status").textContent = JSON.stringify({
version: L.version, size, anchor,
observedImageSize: [ir.width, ir.height], displacement
}, null, 2);
})();
</script>
</html>Показанное состояние должно сообщить anchor: [36, 58], размер изображения [60, 80] и нулевое смещение. Измените size на [90, 120]: тот же расчёт даст [54, 87]. Чтобы использовать плотную рамку, измените и viewBox SVG, и массив JavaScript viewBox на 0 0 60 80. Они должны совпадать.
Контур включает круглое внутреннее очертание с fill-rule="evenodd", создавая настоящее отверстие. Правило объясняет справочник MDN по fill-rule. Пример не добавляет границу или преобразование контейнеру карты; они потребовали бы осторожности при сравнении прямоугольников области просмотра с координатами контейнера.
Проверьте целевую среду перед изменением графики
Если маркер всё ещё не попадает в координату, проверяйте по порядку:
- Подтвердите загрузку SVG-изображения и нужную пиксельную ширину и высоту элемента маркера.
- Найдите настоящий исходный кончик с учётом преобразований внутри SVG.
- Проверьте полный viewBox, его минимальные координаты и режим вписывания.
- Пересчитывайте якорь при изменении рамки или размера значка.
- На время теста удалите посторонние переопределения CSS, затем ещё раз проверьте конечное оформление и целевые размеры отображения.
При этой проверке фиксируйте географическую координату. Её перемещение для компенсации полей изображения скроет ошибку размещения графики.
Это процесс браузерного маркера. SVG-маркер QGIS использует собственные настройки целевой среды и параметризованный синтаксис окрашивания; сохраняйте обычный мастер SVG для общего браузерного использования.
Если значок существует только в PNG или JPG, а нужны редактируемые контуры, преобразуйте графику с PerfectVector, проверьте отверстие и кончик и сохраните чистый мастер SVG. Затем рассчитайте якорь Leaflet из конечной исходной рамки. Векторизация готовит графику; код карты задаёт местоположения и размещение. Если проблема лишь в смещении существующего SVG, исправляйте рамку или якорь напрямую. Руководство по векторизации изображений объясняет, когда полезно преобразование растрового исходника.
Частые вопросы
iconAnchor — это широта и долгота? Нет. Это пиксельная позиция внутри показанного изображения значка, измеренная от его верхнего левого угла. Географическая позиция маркера задаётся отдельно.
Почему SVG с полями остаётся маленьким после исправления якоря? Пустая исходная рамка по-прежнему занимает часть прямоугольника изображения. Исправление якоря размещает кончик; оно не удаляет пустое место и не увеличивает закрашенную фигуру.
Нужно ли всегда использовать половину ширины и полную высоту? Используйте эту пару только тогда, когда видимая точка прикрепления действительно там. Нашим асимметричной метке и исходнику с полями нужны другие значения, хотя оба — графика маркеров местоположения.
Исправляет ли преобразование PNG в SVG местоположение маркера? Преобразование может дать редактируемую графику. Конечная исходная рамка, показываемые размеры, якорь и координаты карты по-прежнему определяют размещение маркера.
Источники
- Leaflet — Справочник Icon — Пиксельные размеры, начало отсчёта якоря, стандартное центрирование и географическое совмещение.
- Leaflet — Маркеры с собственными значками — Создание значка и назначение его маркеру.
- Leaflet — Загрузка — Стабильный выпуск и ссылки на его закреплённые JavaScript и CSS.
- MDN — viewBox — Прямоугольник исходных координат для двух рамок.
- MDN — preserveAspectRatio — Равномерное вписывание и центрирование свободного места для xMidYMid meet.
- MDN — fill-rule — Чётно-нечётное правило для прозрачного отверстия.
Ещё из блога

SVG в узлах Cytoscape.js: вписывание и отступы
Впишите собственную SVG-графику в узлы Cytoscape.js с рабочим примером. Сравните пустое место исходника, contain и cover, затем проверьте загрузку и размещение подписей.

Измеряйте длину контура SVG, не путая масштаб
Измеряйте контуры SVG в пользовательских единицах браузера, учитывайте преобразования и номинальный размер, отличайте сумму контуров от pathLength и хода машины.
