Перейти к содержанию

Геолокация по запросу: разрешения и точность

Оглавление серии «Карты для веба» · Урок 8 из 12.

Публичную камеру можно сохранить в ссылке, но личное положение посетителя требует другого договора. В этом уроке добавим кнопку «Использовать моё положение». До её нажатия приложение не обращается к Geolocation API. Полученную точку будем хранить только в памяти, показывать вместе с оценкой точности и удалять отдельным действием.

Это не функция постоянного слежения за перемещениями. Мы запрашиваем один результат через getCurrentPosition, а не подписываемся на watchPosition. Для каталога «Рядом» такой шаг достаточен: человек может выбрать исходную точку, а в следующем уроке — искать места в радиусе. Отказ в разрешении не должен мешать чтению каталога.

Что нужно браузеру для определения положения

Geolocation API требует безопасного контекста и разрешения пользователя. На публичном сайте это обычно HTTPS; локальная разработка зависит от того, признаёт ли браузер выбранный адрес безопасным контекстом. Кроме того, доступ может быть ограничен Permissions-Policy или окружением встроенного документа. Поэтому наличие кнопки не гарантирует доступность функции.

Проверяем window.isSecureContext и navigator.geolocation до вызова. Если функция недоступна, показываем понятное сообщение. Не предлагаем пользователю отключить защиту браузера, чтобы продолжить чтение. В следующем шаге появится ручной выбор центра карты, который не требует определения позиции устройства.

Системный запрос разрешения принадлежит браузеру. Мы не можем считать нажатие собственной кнопки автоматическим разрешением на все последующие способы использования данных. Текст рядом с кнопкой должен объяснять назначение: точка нужна текущему поиску, не сохраняется в адресе и не отправляется нашему серверу. Отдельно нужно описать запросы к тайловому поставщику, потому что область отображения может раскрывать приблизительное положение.

Получим ограниченный объект позиции

Создайте src/location.js. Ниже полный модуль восьмого шага:

export function requestPosition() {
  return new Promise((resolve, reject) => {
    if (!window.isSecureContext || !navigator.geolocation) {
      reject(new Error('Геолокация недоступна. Можно выбрать центр карты вручную.'));
      return;
    }
    navigator.geolocation.getCurrentPosition(position => {
      const c = position.coords;
      if (![c.latitude, c.longitude, c.accuracy].every(Number.isFinite)
        || Math.abs(c.latitude) > 85 || Math.abs(c.longitude) > 180 || c.accuracy < 0) {
        reject(new Error('Получены неподдерживаемые координаты.'));
        return;
      }
      resolve({ latitude: c.latitude, longitude: c.longitude, accuracy: c.accuracy,
        source: 'device' });
    }, error => {
      const messages = {
        1: 'Доступ к положению не разрешён.',
        2: 'Устройство не смогло определить положение.',
        3: 'Время ожидания положения истекло.'
      };
      reject(new Error(messages[error.code] || 'Не удалось получить положение.'));
    }, { enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 });
  });
}

Promise делает результат удобным для контроллера, но не добавляет в API возможность отменить системный запрос через AbortController. В модуле остаются два callback: успешный результат и отказ. Мы возвращаем только нужные поля, а не весь объект браузера. Это уменьшает количество случайно доступных данных и явно определяет, что использует приложение.

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

maximumAge: 60000 допускает недавний результат, если браузер может его использовать. enableHighAccuracy: false не просит наиболее затратный режим определения положения. Это пожелание API, а не гарантированная технология измерения: браузер и устройство решают, какие источники доступны. Для задачи поиска ближайших учебных мест нам не нужен обещанный сантиметровый результат.

timeout ограничивает ожидание получения позиции согласно поведению API; его нельзя выдавать за точный секундомер всего взаимодействия с системным разрешением. Пользователь может долго принимать решение в диалоге. В интерфейсе показываем состояние ожидания и оставляем возможность убрать исходную точку. Кнопка определения временно отключается, чтобы не запустить несколько одинаковых запросов подряд.

Точность — часть результата

Координаты обычно выглядят точными из-за большого числа десятичных знаков. Однако coords.accuracy описывает оценку горизонтальной точности в метрах. Мы показываем её текстом и окружностью на карте через L.circle, где радиус тоже задаётся в метрах. Это напоминает пользователю, что точка не является подтверждённым адресом помещения.

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

Для отображения applyPosition сначала запоминает публичную камеру, затем включает privateView, назначает точку и перемещает карту. Такой порядок важен: перемещение вызывает события, и запись нового центра в адрес должна быть запрещена уже до этих событий. Если установить признак после panTo, часть приватного состояния может попасть в URL.

Не применим устаревший ответ

Пока браузер определяет позицию, человек может нажать «Убрать положение». Сам запрос не отменяется, но его будущий результат уже не должен возвращать личную точку на карту. Для этого используем счётчик positionEpoch:

const epoch = ++positionEpoch;
const position = await requestPosition();
if (epoch === positionEpoch) applyPosition(position);

При удалении точки увеличиваем счётчик ещё раз. Старый callback получит результат, но не применит его к интерфейсу. Это защита состояния от запоздалого ответа, а не остановка работы системного сервиса. Та же идея пригодится при импорте файла и других асинхронных действиях, если пользователь успевает принять новое решение.

Ошибки тоже показываем только для текущего запроса. Иначе запоздалое сообщение могло бы затереть успешный результат ручного выбора. Сам текст ошибки формируем по известному коду, не выводим внутренние сведения браузера и координаты в журнал. Кнопка снова разрешается в finally, когда соответствующее обращение завершится.

Удаление точки возвращает публичный режим

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

В этой серии позиция не записывается в localStorage, GeoJSON, URL или cookies. При перезагрузке документа её нет. Это уменьшает срок хранения в приложении, но не отменяет сетевые наблюдения третьих сторон. Если подложка уже включена и камера перейдёт к позиции, поставщик увидит запрашиваемую область. Для продукта с более строгими требованиями можно оставить подложку выключенной при личном поиске или выбрать инфраструктуру с другим договором.

Восьмой шаг показывает положение и его оценку, но ещё не ограничивает список расстоянием. Фильтр по категории и тексту продолжает работать как раньше. В следующем уроке полученная точка станет центром радиуса. Такое разделение позволяет сначала понять разрешение, источник и жизненный цикл данных, затем — математический смысл «рядом».

При самостоятельном выполнении попробуйте отказать в разрешении: каталог должен остаться читаемым. Затем начните запрос и удалите точку до ответа. Запоздалый результат не должен вернуть её. Наконец, при разрешённом доступе сравните адрес до и после перемещения: личный центр не должен записаться в fragment. Здесь описаны ожидаемые сценарии; геолокация при подготовке материала не запускалась.

Полный снимок этого шага находится в папке lesson-08 архива серии. При подготовке материала пример не запускался; описанные эффекты ожидаются по разобранному коду и требуют самостоятельной проверки в браузере.