PerfectVector
Автор: Irene Kim

Маркеры SVG в Leaflet: совместите кончик с iconAnchor

Размещайте собственные SVG-маркеры в Leaflet, сопоставляя видимый кончик с iconAnchor. Учитывайте поля viewBox, вписывание с сохранением пропорций и изменение размера значка.

9 мин чтения
На этой странице

Для собственного SVG-маркера Leaflet задайте iconSize равным размерам показываемого изображения, а iconAnchor — пиксельной позиции видимого кончика внутри него. Измеряйте от верхнего левого угла изображения. Прозрачные поля SVG входят в эти размеры, поэтому половина ширины и полная высота могут не попасть в кончик асимметричной метки или метки с полями.

В руководстве используется оригинальная метка, чей кончик находится справа от центра её тела. Мы сравниваем две исходные рамки в двух размерах значка на браузерной карте без тайлов. Если чистая SVG-графика уже есть, используйте её напрямую: размещению якоря векторизация не нужна.

Отделите графику, прямоугольник изображения и точку карты

Здесь встречаются три системы координат. Контур SVG описывает графику в исходных единицах. iconSize задаёт её показываемый прямоугольник в пикселях. Leaflet совмещает точку внутри этого прямоугольника, заданную iconAnchor, с географическим положением маркера. Справочник Icon определяет эти параметры и начало отсчёта якоря в верхнем левом углу.

Обычная метка может доходить до середины нижнего края, но ваша графика может заканчиваться в другом месте. Описанное в документации значение Leaflet по умолчанию — центральный якорь, если размер указан. Значения нижнего центра в нашем эксперименте — явно проверяемое допущение, а не это значение по умолчанию.

Официальный урок по собственным значкам показывает передачу экземпляра L.icon маркеру. Для импортированной графики дополнительно нужно найти точку внутри изображения, которая должна касаться координаты карты.

Видимый кончик и якорь
Концептуальное сравнение асимметричной метки местоположения на холсте с полями: кончик сначала смещён от перекрестия, затем совпадает с ним
Иллюстрация: учитывайте всю рамку изображения при размещении видимого кончика. Измеренный браузерный эксперимент ниже использует отдельно созданные файлы SVG.

Переведите исходный кончик в пиксели значка

Кончик нашей метки находится в исходной координате (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 8060×80[40, 80]
Плотная, 0 0 60 8090×120[60, 120]
С полями, -20 -10 100 12060×80[36, 58]
С полями, -20 -10 100 12090×120[54, 87]

Эти расчёты относятся к нашему изображению без преобразований и явному центральному meet. Другое выравнивание, slice или none требует соответствующего преобразования координат. Наше руководство по пропорциям описывает эти варианты. Дополнительные преобразования CSS изображения маркера тоже нуждаются в отдельной проверке.

Проверьте размещение на карте без тайлов

Мы использовали Leaflet 1.9.4 — стабильный выпуск, указанный на официальной странице загрузки. Эксперимент локально загружает JavaScript и CSS закреплённой версии и не добавляет тайловый слой. Каждая карта проецирует координату [0, 0] на синее перекрестие и показывает одно SVG-изображение рядом с этой опорной точкой.

Проверка исходников нашла один одинаковый составной контур и отсутствие встроенного растра в каждом SVG. Размеры изображения в браузере были 60×80 и 90×120 пикселей, как запрошено. Используя наблюдаемый прямоугольник каждого изображения, мы сравнили рассчитанную позицию кончика с проецируемой географической точкой:

ИсходникРазмер значкаОшибка при предполагаемом якоре внизу по центруОшибка с рассчитанным якорем кончика
Плотный60×8010 px вправо0 px
Плотный90×12015 px вправо0 px
С полями60×806 px вправо, 22 px вверх0 px
С полями90×1209 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. Пример не добавляет границу или преобразование контейнеру карты; они потребовали бы осторожности при сравнении прямоугольников области просмотра с координатами контейнера.

Проверьте целевую среду перед изменением графики

Если маркер всё ещё не попадает в координату, проверяйте по порядку:

  1. Подтвердите загрузку SVG-изображения и нужную пиксельную ширину и высоту элемента маркера.
  2. Найдите настоящий исходный кончик с учётом преобразований внутри SVG.
  3. Проверьте полный viewBox, его минимальные координаты и режим вписывания.
  4. Пересчитывайте якорь при изменении рамки или размера значка.
  5. На время теста удалите посторонние переопределения CSS, затем ещё раз проверьте конечное оформление и целевые размеры отображения.

При этой проверке фиксируйте географическую координату. Её перемещение для компенсации полей изображения скроет ошибку размещения графики.

Это процесс браузерного маркера. SVG-маркер QGIS использует собственные настройки целевой среды и параметризованный синтаксис окрашивания; сохраняйте обычный мастер SVG для общего браузерного использования.

Если значок существует только в PNG или JPG, а нужны редактируемые контуры, преобразуйте графику с PerfectVector, проверьте отверстие и кончик и сохраните чистый мастер SVG. Затем рассчитайте якорь Leaflet из конечной исходной рамки. Векторизация готовит графику; код карты задаёт местоположения и размещение. Если проблема лишь в смещении существующего SVG, исправляйте рамку или якорь напрямую. Руководство по векторизации изображений объясняет, когда полезно преобразование растрового исходника.

Частые вопросы

iconAnchor — это широта и долгота? Нет. Это пиксельная позиция внутри показанного изображения значка, измеренная от его верхнего левого угла. Географическая позиция маркера задаётся отдельно.

Почему SVG с полями остаётся маленьким после исправления якоря? Пустая исходная рамка по-прежнему занимает часть прямоугольника изображения. Исправление якоря размещает кончик; оно не удаляет пустое место и не увеличивает закрашенную фигуру.

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

Исправляет ли преобразование PNG в SVG местоположение маркера? Преобразование может дать редактируемую графику. Конечная исходная рамка, показываемые размеры, якорь и координаты карты по-прежнему определяют размещение маркера.

Источники

  1. Leaflet — Справочник Icon — Пиксельные размеры, начало отсчёта якоря, стандартное центрирование и географическое совмещение.
  2. Leaflet — Маркеры с собственными значками — Создание значка и назначение его маркеру.
  3. Leaflet — Загрузка — Стабильный выпуск и ссылки на его закреплённые JavaScript и CSS.
  4. MDN — viewBox — Прямоугольник исходных координат для двух рамок.
  5. MDN — preserveAspectRatio — Равномерное вписывание и центрирование свободного места для xMidYMid meet.
  6. MDN — fill-rule — Чётно-нечётное правило для прозрачного отверстия.

Ещё из блога

PerfectVector

Начните с более чистого SVG,
который проще редактировать

Без банковской карты