Карта на веб-странице: координаты, контейнер и Leaflet
Оглавление серии «Карты для веба» · Урок 1 из 12.
Каталог мест обычно начинается со списка названий и адресов. Карта помогает ответить на другой вопрос: как эти места расположены относительно друг друга? В этой серии создадим «Рядом» — небольшой каталог вымышленных мастерских и библиотек. Сначала подготовим поверхность карты, затем добавим данные, выбор, фильтры и поиск в радиусе. Такой пример подходит для страницы услуг, справочника площадок или списка учебных мероприятий, если позднее заменить учебные записи проверенными данными.
В первом уроке не будем искать адреса через внешний сервис. Нам нужны только HTML, CSS, немного JavaScript и библиотека Leaflet. Результатом станет контейнер с координатной камерой, которую можно перемещать и масштабировать. Географическую подложку подключим отдельно: библиотека отображения и поставщик картографических изображений решают разные задачи.
Из каких частей складывается карта
Представьте обычную фотографию, которую увеличивают в редакторе. У изображения есть собственные размеры, у видимой области — размеры окна, а у пользователя — выбранный масштаб. У карты похожее устройство, но содержимое привязано к координатам на Земле. Камера определяет центр и масштаб; слои описывают, что должно быть нарисовано в этом пространстве. Один слой может показывать подложку, другой — точки каталога, третий — окружность поиска.
Leaflet управляет этим взаимодействием в браузере. Он создаёт внутренние контейнеры, переводит географические координаты в положение элементов и сообщает о перемещениях. Сам по себе он не знает, где находится учебная мастерская, и не выдаёт список организаций. Эти сведения передаст наше приложение. Такое разделение позволит позднее поменять подложку без изменения идентификаторов мест и правил фильтрации.
Карта не заменяет текстовую страницу. Пользователь может читать со вспомогательной технологией, отключить JavaScript или оказаться в сети, где внешний поставщик недоступен. В следующих уроках добавим список тех же объектов. Сейчас важно оставить в HTML заголовок, объяснение учебного набора и область статуса: они существуют независимо от того, появились ли географические изображения.
Зафиксируем профиль библиотеки
Используем Leaflet 1.9.4 и обычные браузерные ES modules для собственного кода. Это выбранный профиль курса, а не требование применять одну версию в любом будущем проекте. Он подтверждён манифестом тега и примером этого же тега.
Это уточнение полезно: текущая ветка документации показывает также API 2.0 alpha с именованными импортами. В наших примерах фабрики вызываются через глобальный объект L, который создаёт классический файл Leaflet 1.9.4. Собственные файлы при этом остаются модулями. Если смешать пример с new LeafletMap из другой версии и файл с глобальным L, ошибка появится до первого маркера. Обновление профиля потребует отдельного прохода по импорту, созданию объектов и совместимости дополнений.
Библиотека и её стили подключены с закреплённого URL unpkg. При самостоятельном открытии примера этот CDN получает обычные сетевые запросы браузера. Для будущего выпуска можно хранить проверенные файлы на своём домене и добавить проверку целостности; в архиве курса зависимости не устанавливались и не скачивались для исполнения. Пока нам важно видеть точную версию прямо в адресе, чтобы ссылка без номера не изменила API незаметно.
Подготовим документ
Создайте папку проекта и файл index.html. Ниже полный документ первого шага; здесь ещё нет списка мест, геолокации и тайлов.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Рядом — учебный каталог мест</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="style.css">
<script defer src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" src="src/main.js"></script>
</head>
<body>
<main>
<h1>Рядом</h1>
<p class="notice">Все места вымышлены. Leaflet загружается с unpkg.
Подложка OSM после нажатия кнопки получает запросы с вашей областью просмотра.
Геолокация запрашивается отдельно, по кнопке.</p>
<div class="controls">
</div>
<p id="status" role="status">Учебная карта без подложки.</p>
<div class="workspace">
<div id="map" aria-label="Карта вымышленных мест"></div>
</div>
<noscript>Для карты нужен JavaScript. Это учебный клиентский пример;
текстовую серверную версию каталога рассматриваем в уроке о выпуске.</noscript>
</main>
</body>
</html>
CSS Leaflet описывает внутренние элементы карты. Наш style.css отвечает за общий документ и размер области. Существенная часть собственного файла выглядит так:
#map {
min-height: 28rem;
background: #e4e1e9;
}
Без заданной высоты пустой блок может оказаться почти незаметным. Библиотека не должна угадывать, сколько места вы отвели карте среди текста страницы. На широком экране последующие уроки расположат список рядом, на узком — сверху. Уже поэтому полезно управлять размером через CSS, а не вставлять фиксированные пиксели во множество вызовов JavaScript.
Файлы модулей нужно открывать через локальный HTTP-сервер. Прямой file:// не воспроизводит обычную загрузку сайта и может ограничивать импорты. В рамках подготовки курса сервер для примера не запускался. Выберите его самостоятельно, когда решите выполнять урок: он должен отдавать .js с корректным типом содержимого, а пути src/main.js и src/map.js должны соответствовать расположению документа.
Если Python 3 уже установлен, простой вариант для будущего ручного выполнения — перейти в папку lesson-01 и использовать стандартный HTTP-сервер:
python3 -m http.server 8765 --bind 127.0.0.1
Затем откройте http://127.0.0.1:8765/. Привязка к loopback ограничивает доступ вашим компьютером; это локальный просмотр статических файлов, а не публичный хостинг. После завершения остановите процесс клавишами Ctrl+C. Для следующего состояния берите другую целую папку, чтобы файлы разных шагов не смешались.
Создадим камеру
В src/map.js находится полный код создания карты:
export const map = L.map('map', {
scrollWheelZoom: false, minZoom: 3, maxZoom: 19
}).setView(
[53.9, 27.5667], 13
);
Первый аргумент L.map указывает на контейнер. Параметры задают пределы масштаба и отключают увеличение обычным колесом мыши. Последнее решение удобно для страницы, на которой пользователь прежде всего прокручивает текст: случайное движение над картой не должно перехватывать привычный жест. Кнопки увеличения и клавиатурное управление сохраняются. В продуктовом интерфейсе это правило можно пересмотреть после наблюдения за реальными действиями пользователей.
setView получает широту, долготу и масштаб. Координаты [53.9, 27.5667] выбраны как условный центр учебной области возле Минска; они не обозначают организацию из каталога. Масштаб 13 — значение в шкале карты, а не процент увеличения и не расстояние в километрах. Числа 3 и 19 ограничивают допустимое приближение в выбранном профиле; такие же границы позднее используем при чтении адреса страницы.
Обратите внимание на порядок: Leaflet здесь принимает широту, затем долготу. В GeoJSON порядок обратный, и ему посвятим отдельный урок. Сейчас не создавайте безымянные массивы координат в разных местах приложения. Для начальной камеры достаточно одного выражения; в модели места позднее появится явно определённый формат, который преобразуем на границе отображения.
Файл src/main.js импортирует созданную карту и оставляет сообщение о работе без подложки. При ожидаемом выполнении будут видны фон, элементы управления и область карты, но улицы не появятся: тайловый слой отсутствует намеренно. Это позволяет отличить ошибку контейнера от отказа сетевого поставщика.
Разберём два разных сбоя
Если вместо карты видно пустое место без элементов управления, сначала проверьте существование #map, высоту и загрузку Leaflet. Ошибка «L не определён» указывает на библиотеку или порядок загрузки; увеличение высоты её не исправит. Если элементы управления видны, но нет улиц, первый шаг курса пока работает именно так: никакой слой не запрашивает изображения.
Когда контейнер становится видимым после скрытой вкладки или меняет размер без обычного изменения окна, библиотеке может понадобиться invalidateSize(). Это сообщение о новых размерах области, а не повторная инициализация карты. Создавать второй L.map для уже занятого контейнера нельзя: жизненным циклом одного экземпляра должен управлять один владелец. В нашем документе контейнер виден сразу, поэтому дополнительный вызов пока не требуется.
Измените в копии первого шага масштаб с 13 на 12 и затем центр на другую учебную координату. Ожидается изменение камеры при сохранении размеров HTML-блока. Вы получили основу, к которой можно добавлять независимо выбранные слои. В следующем уроке разберём, откуда появляются тайлы и какие условия связаны с их поставщиком.
Полный снимок этого шага находится в папке lesson-01 архива серии. При подготовке материала пример не запускался; описанные эффекты ожидаются по разобранному коду и требуют самостоятельной проверки в браузере.