Конвертация координат

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

Mapbox GL JS предоставляет набор инструментов для преобразования между различными системами координат:

  • географические координаты (LngLat);
  • экранные координаты (Point);
  • мировые координаты карты;
  • координаты в проекции Web Mercator;
  • координаты внутри контейнера карты.

Подобные преобразования используются для:

  • определения координат точки по клику мыши;
  • размещения пользовательских элементов поверх карты;
  • расчёта расстояний на экране;
  • синхронизации картографических объектов с DOM-элементами;
  • построения пользовательских инструментов рисования и измерения.

Географические координаты

Основной формат координат в Mapbox GL JS представлен классом LngLat.

Пример создания объекта:

const point = new mapboxgl.LngLat(37.6176, 55.7558);

Параметры:

Параметр Значение
Longitude Долгота
Latitude Широта

Важно помнить порядок:

[lng, lat]

а не

[lat, lng]

Правильно:

[37.6176, 55.7558]

Неправильно:

[55.7558, 37.6176]

Класс LngLat

Многие методы карты возвращают экземпляр LngLat.

Получение центра карты:

const center = map.getCenter();

console.log(center.lng);
console.log(center.lat);

Результат:

LngLat {
    lng: 37.6176,
    lat: 55.7558
}

Конвертация в массив:

const coords = center.toArray();

console.log(coords);

Результат:

[37.6176, 55.7558]

Конвертация в строку:

console.log(center.toString());

Результат:

"LngLat(37.6176, 55.7558)"

Экранные координаты Point

Для работы с экраном используется класс Point.

Он представляет положение объекта внутри контейнера карты в пикселях.

Структура:

Point {
    x: 250,
    y: 400
}

Где:

  • x — смещение по горизонтали;
  • y — смещение по вертикали.

Начало координат располагается в левом верхнем углу контейнера карты.

Схема:

(0,0)
 ┌───────────────────► X
 │
 │
 │
 ▼
 Y

Метод project()

Метод project() преобразует географические координаты в экранные.

Синтаксис:

map.project(lngLat)

Пример:

const pixel = map.project([37.6176, 55.7558]);

console.log(pixel);

Результат:

Point {
    x: 532,
    y: 284
}

После этого координаты можно использовать для размещения DOM-элементов.

Пример:

const markerElement = document.getElementById('custom-marker');

const point = map.project([37.6176, 55.7558]);

markerElement.style.left = `${point.x}px`;
markerElement.style.top = `${point.y}px`;

Метод unproject()

Обратное преобразование выполняется методом unproject().

Синтаксис:

map.unproject(point)

Пример:

const lngLat = map.unproject([500, 300]);

console.log(lngLat);

Результат:

LngLat {
    lng: 37.6,
    lat: 55.7
}

Таким образом можно определить координаты на карте по известному пикселю экрана.


Преобразование координат по клику

Наиболее распространённый сценарий связан с обработкой событий мыши.

map.on('click', (event) => {
    console.log(event.lngLat);
});

Объект события содержит уже вычисленные координаты.

Также доступны экранные координаты:

map.on('click', (event) => {
    console.log(event.point);
});

Результат:

Point {
    x: 600,
    y: 350
}

Преобразование выполняется автоматически:

map.on('click', (event) => {
    const pixel = event.point;

    const coords = map.unproject(pixel);

    console.log(coords);
});

Взаимное преобразование Point и LngLat

Часто требуется получить обе формы координат одновременно.

map.on('mousemove', (event) => {

    const lngLat = event.lngLat;

    const point = map.project(lngLat);

    console.log(lngLat);
    console.log(point);

});

Схема преобразований:

LngLat
   │
   ▼
project()
   │
   ▼
Point
   │
   ▼
unproject()
   │
   ▼
LngLat

Получение координат курсора

Во время перемещения мыши можно постоянно отслеживать положение указателя.

map.on('mousemove', (event) => {

    const { lng, lat } = event.lngLat;

    console.log(
        lng.toFixed(6),
        lat.toFixed(6)
    );

});

Вывод:

37.617420 55.755790
37.617430 55.755800
37.617450 55.755810

Подобный механизм используется для создания координатной строки в интерфейсе ГИС.


Работа с координатами контейнера карты

Размеры контейнера:

const canvas = map.getCanvas();

console.log(canvas.width);
console.log(canvas.height);

Центр контейнера:

const centerPoint = {
    x: canvas.width / 2,
    y: canvas.height / 2
};

Получение географической точки в центре:

const centerLngLat = map.unproject(centerPoint);

console.log(centerLngLat);

Определение географической области экрана

Для вычисления видимой области необходимо преобразовать углы контейнера.

Левый верхний угол:

const northWest = map.unproject([0, 0]);

Правый нижний угол:

const canvas = map.getCanvas();

const southEast = map.unproject([
    canvas.width,
    canvas.height
]);

Получение координат:

console.log(northWest);
console.log(southEast);

Такой подход используется при реализации пользовательских запросов к серверу по видимой области карты.


Использование getBounds()

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

const bounds = map.getBounds();

Получение углов:

const northEast = bounds.getNorthEast();
const southWest = bounds.getSouthWest();

Получение координат:

console.log(northEast.lng);
console.log(northEast.lat);

console.log(southWest.lng);
console.log(southWest.lat);

Объект:

LngLatBounds

представляет географическую область текущего окна карты.


Конвертация в формат GeoJSON

Mapbox GL JS активно использует GeoJSON.

Преобразование точки:

const lngLat = map.getCenter();

const geojson = {
    type: "Point",
    coordinates: lngLat.toArray()
};

Результат:

{
  "type": "Point",
  "coordinates": [
    37.6176,
    55.7558
  ]
}

Конвертация из GeoJSON

Исходная геометрия:

const feature = {
    type: "Feature",
    geometry: {
        type: "Point",
        coordinates: [37.6176, 55.7558]
    }
};

Создание объекта LngLat:

const lngLat = new mapboxgl.LngLat(
    feature.geometry.coordinates[0],
    feature.geometry.coordinates[1]
);

Упрощённый вариант:

const lngLat = feature.geometry.coordinates;

Поскольку большинство методов карты принимают массив координат напрямую.


Конвертация между массивом и объектом

Массив:

const coordinates = [37.6176, 55.7558];

Объект:

const lngLat = mapboxgl.LngLat.convert(
    coordinates
);

Результат:

LngLat {
    lng: 37.6176,
    lat: 55.7558
}

Метод также работает с объектами:

const point = mapboxgl.LngLat.convert({
    lng: 37.6176,
    lat: 55.7558
});

Web Mercator и внутренние координаты карты

Mapbox GL JS использует проекцию Web Mercator.

Она позволяет представить поверхность Земли на плоскости.

Особенности:

  • стандарт де-факто для веб-карт;
  • используется OpenStreetMap;
  • используется Google Maps;
  • используется Mapbox;
  • используется большинством тайловых сервисов.

Географические координаты автоматически преобразуются в меркаторские значения при рендеринге.

Процесс выглядит следующим образом:

Longitude/Latitude
        │
        ▼
Web Mercator
        │
        ▼
World Coordinates
        │
        ▼
Screen Pixels

Разработчику обычно не требуется выполнять эти вычисления вручную, поскольку движок делает их автоматически.


MercatorCoordinate

Для работы с трёхмерной графикой предусмотрен класс MercatorCoordinate.

Создание объекта:

const mercator =
    mapboxgl.MercatorCoordinate
        .fromLngLat(
            [37.6176, 55.7558]
        );

Получение значений:

console.log(mercator.x);
console.log(mercator.y);
console.log(mercator.z);

Такие координаты используются:

  • при интеграции Three.js;
  • при работе с пользовательскими WebGL-слоями;
  • при создании 3D-объектов;
  • при визуализации моделей зданий.

Конвертация координат для пользовательских маркеров

Стандартные HTML-маркеры часто требуют ручного позиционирования.

const point = map.project([
    37.6176,
    55.7558
]);

Размещение:

element.style.transform =
    `translate(${point.x}px, ${point.y}px)`;

При перемещении карты необходимо обновлять координаты.

map.on('move', updateMarker);

Функция:

function updateMarker() {

    const point = map.project([
        37.6176,
        55.7558
    ]);

    element.style.transform =
        `translate(${point.x}px, ${point.y}px)`;
}

Определение расстояния между экранными точками

После преобразования географических координат в пиксели становится возможным вычислять экранные расстояния.

const p1 = map.project([
    37.6176,
    55.7558
]);

const p2 = map.project([
    37.65,
    55.78
]);

Расчёт:

const distance = Math.sqrt(
    Math.pow(p2.x - p1.x, 2) +
    Math.pow(p2.y - p1.y, 2)
);

console.log(distance);

Результат:

245.37

Значение будет выражено в пикселях текущего масштаба карты.


Особенности преобразования координат при изменении масштаба

Экранные координаты зависят от:

  • масштаба (zoom);
  • центра карты;
  • угла поворота (bearing);
  • наклона (pitch).

Например:

const point = map.project([
    37.6176,
    55.7558
]);

После изменения масштаба:

map.zoomTo(12);

тот же географический объект получит другие экранные координаты.

По этой причине результаты project() нельзя кэшировать на длительное время без пересчёта.


Типичные ошибки при конвертации координат

Перепутанный порядок координат

Неправильно:

[55.7558, 37.6176]

Правильно:

[37.6176, 55.7558]

Использование устаревших экранных координат

После перемещения карты координаты необходимо пересчитывать.

Неправильно:

const point = map.project(coords);

// использование спустя несколько минут

Правильно:

map.on('move', () => {
    const point = map.project(coords);
});

Игнорирование вращения карты

При ненулевом значении bearing экранные координаты изменяются относительно стандартного положения карты.

map.setBearing(45);

После поворота результаты:

map.project(...)

будут отличаться от значений, полученных до вращения.


Неверное использование координат окна браузера

Координаты объекта event.clientX относятся ко всему окну браузера.

document.addEventListener('click', (event) => {
    console.log(event.clientX);
});

Для картографических вычислений предпочтительно использовать:

event.point

из событий Mapbox GL JS, поскольку они уже привязаны к контейнеру карты.