company_banner

Работа с GeoJSON в среде Node.js: практическое знакомство

Original author: Valeri Karpov
  • Translation
GeoJSON — это стандартизованный формат представления географических структур данных, основанный на JSON. Существует множество замечательных инструментов для визуализации GeoJSON-данных. При этом данный формат хорош не только в деле хранения координат неких точек. Он, помимо точек, позволяет описывать и другие объекты: линии, полигоны, коллекции объектов.



Точки — объекты Point


GeoJSON-точка выглядит так:

{
  "type": "Point",
  "coordinates": [-80.1347334, 25.7663562]
}

Эта точка представляет парк в Майами-Бич, штат Флорида, США. Визуализировать эту точку на карте легко можно с помощью проекта geojson.io.


Точка на карте

Важно отметить, что координата в свойстве coordinates записывается в формате [lng, lat]. Долгота в GeoJSON идёт перед широтой. Это так из-за того, что долгота представляет направление «восток-запад» (ось x на типичной карте), а широта — направление «север-юг» (ось y на типичной карте). Авторы GeoJSON стремились к сохранению порядка координат x, y.

Типичный пример использования точек GeoJSON — геокодирование — преобразование адресов наподобие «429 Lenox Ave, Miami Beach, FL» в координаты, выраженные долготой и широтой. Например, мы пользуемся API геокодирования Mapbox. Для обращения к этому API нужно выполнить HTTP-запрос к следующей конечной точке:

https://api.mapbox.com/geocoding/v5/mapbox.places/429%20lenox%20ave%20miami.json?access_token=pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g&cachebuster=1581993735895&autocomplete=true

В ответ придёт такой код:

{"type":"FeatureCollection","query":["429","lenox","ave","miami"],"features":[{"id":"address.8052276751051244","type":"Feature","place_type":["address"],"relevance":1,"properties":{"accuracy":"rooftop"},"text":"Lenox Avenue","place_name":"429 Lenox Avenue, Miami Beach, Florida 33139, United States","center":[-80.139145,25.77409],"geometry":{"type":"Point","coordinates":[-80.139145,25.77409]}, ...}

Если присмотреться к ответу, то окажется, что features[0].geometry в JSON-коде — это GeoJSON-точка:

{"type":"Point","coordinates":[-80.139145,25.77409]}


Визуализация координат

API статических карт Mapbox — это отличный инструмент для вывода точек на картах. Ниже показан скрипт, который декодирует переданную ему строку и возвращает URL на изображение, которое показывает первый результат поиска.

const axios = require('axios');

async function search(str) {
  const geocoderUrl = 'https://api.mapbox.com/geocoding/v5/mapbox.places/' +
    encodeURIComponent(str) +
    '.json?access_token=' +
    'pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g';

  const res = await axios.get(geocoderUrl).then(res => res.data);
  const point = res.features[0].geometry;

  return 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/' +
    'pin-l-1+333(' + point.coordinates[0] + ',' + point.coordinates[1] + ')/' +
    point.coordinates[0] + ',' + point.coordinates[1] +
    ',14.25,0,0/600x600/' +
    '?access_token=pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g';
}

search('429 Lenox Ave, Miami Beach').then(res => console.log(res));


Пример визуализации точки на карте

Линии — объекты LineString


В GeoJSON линии, объекты LineString, представляют массивы координат, описывающие линию на карте. Ниже показан GeoJSON-объект LineString, представляющий приблизительную границу между штатами Калифорния и Орегон в США:

{
  "type": "LineString",
  "coordinates": [[-124.2, 42], [-120, 42]]
}


Визуализация объекта LineString на карте

Линии, при использовании API навигации наподобие Mapbox, применяются для визуализации поэтапного пути между двумя точками. Один из способов представления автомобильного пути из точки [-80.139145,25.77409] (офис WeWork в Майами-Бич) до точки [-80.2752743,25.7938434] (международный аэропорт Майами) заключается в использовании GeoJSON-объекта LineString:

{
  "type": "LineString",
  "coordinates": [
    [-80.139153, 25.774281],
    [-80.13829, 25.774307],
    [-80.142029, 25.774479],
    [-80.148438, 25.772148],
    [-80.151237, 25.772232],
    [-80.172043, 25.78116],
    [-80.177322, 25.787195],
    [-80.185326, 25.787212],
    [-80.189804, 25.785891],
    [-80.19268, 25.785954],
    [-80.202301, 25.789175],
    [-80.207954, 25.788721],
    [-80.223, 25.782646],
    [-80.231026, 25.78261],
    [-80.238007, 25.784889],
    [-80.246025, 25.784403],
    [-80.249611, 25.785175],
    [-80.253166, 25.786049],
    [-80.259262, 25.786324],
    [-80.264038, 25.786186],
    [-80.264221, 25.787256],
    [-80.264214, 25.791618],
    [-80.264221, 25.792633],
    [-80.264069, 25.795443],
    [-80.263397, 25.795652],
    [-80.263786, 25.794928],
    [-80.267723, 25.794926],
    [-80.271141, 25.794859],
    [-80.273163, 25.795704],
    [-80.275009, 25.796482],
    [-80.277481, 25.796461],
    [-80.278435, 25.795622],
    [-80.278061, 25.794088],
    [-80.275276, 25.793804]
  ]
}

Объекты LineString, представляющие собой некие маршруты, могут быть очень сложными. Вышеприведённый объект, например, описывает короткую 15-минутную поездку. Вот как всё это выглядит на карте.


Путь из одной точки в другую

Вот — простой скрипт, который возвращает LineString-представление пути между 2 точками с использованием API directions Mapbox.

const axios = require('axios');

async function directions(fromPt, toPt) {
  const fromCoords = fromPt.coordinates.join(',');
  const toCoords = toPt.coordinates.join(',');
  const directionsUrl = 'https://api.mapbox.com/directions/v5/mapbox/driving/' +
    fromCoords + ';' + toCoords + '?' +
    'geometries=geojson&' +
    'access_token=pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g';

  const res = await axios.get(directionsUrl).then(res => res.data);
  return res.routes[0].geometry;
}

const wework = { type: 'Point', coordinates: [-80.139145,25.77409] };
const airport = { type: 'Point', coordinates: [-80.2752743,25.7938434] };

directions(wework, airport).then(res => {
  console.log(res);
});

Полигоны — объекты Polygon


GeoJSON-полигоны, объекты Polygon, используются для описания замкнутых областей на картах. Это могут быть области, имеющие форму треугольника, квадрата, двенадцатиугольника, или любой другой фигуры с фиксированным количеством сторон. Например, следующий GeoJSON-объект грубо описывает границы штата Колорадо в США:

{
  "type": "Polygon",
  "coordinates": [[
    [-109, 41],
    [-102, 41],
    [-102, 37],
    [-109, 37],
    [-109, 41]
  ]]
}


Визуализация полигона на карте

GeoJSON-полигоны могут использоваться для описания очень сложных форм. Например, некоторое время в Uber использовался единственный GeoJSON-полигон, включающий в себя все 3 основных аэропорта области залива Сан-Франциско.


Сложный GeoJSON-полигон

Правда, надо отметить, GeoJSON-полигоны не могут представлять окружности и эллипсы.

Для чего используются полигоны? Обычно — для описания геозон. Например, представьте себе, что работаете в Uber или в Lyft. Вам нужно показать пользователям, заказывающим поездки из аэропорта, особый экран. Для того чтобы это сделать, нужно будет узнать, находится ли точка, из которой заказывают поездку, в пределах полигона, описывающего аэропорт (или несколько аэропортов как на предыдущем рисунке).

Один из способов проверки нахождения GeoJSON-точки в пределах полигона заключается в использовании npm-модуля Turf. Модуль @turf/boolean-point-in-polygon позволяет узнать о том, находится ли точка в пределах полигона.

const pointInPolygon = require('@turf/boolean-point-in-polygon').default;

const colorado = {
  "type": "Polygon",
  "coordinates": [[
    [-109, 41],
    [-102, 41],
    [-102, 37],
    [-109, 37],
    [-109, 41]
  ]]
};

const denver = {
  "type": "Point",
  "coordinates": [-104.9951943, 39.7645187]
};

const sanFrancisco = {
  "type": "Point",
  "coordinates": [-122.4726194, 37.7577627]
};

// true
console.log(pointInPolygon(denver, colorado));

// false
console.log(pointInPolygon(sanFrancisco, colorado));

Пакет Turf позволяет узнать о том, находится ли точка в пределах полигона, пользуясь средой Node.js. Но что если нас интересует получение таких же сведений путём выполнения запросов к базе данных? В таком случае стоит знать о том, что встроенный оператор MongoDB $geoIntersects поддерживает GeoJSON. Поэтому, например, можно написать запрос, который позволяет выяснить, какому штату США соответствует некая точка на карте:

const mongoose = require('mongoose');

run().catch(err => console.log(err));

async function run() {
  await mongoose.connect('mongodb://localhost:27017/geotest', {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });
  await mongoose.connection.dropDatabase();

  const State = mongoose.model('State', mongoose.Schema({
    name: String,
    location: mongoose.Schema({
      type: String,
      coordinates: [[[Number]]]
    })
  }));

  const colorado = await State.create({
    name: 'Colorado',
    location: {
      "type": "Polygon",
      "coordinates": [[
        [-109, 41],
        [-102, 41],
        [-102, 37],
        [-109, 37],
        [-109, 41]
      ]]
    }
  });

    const denver = {
    "type": "Point",
    "coordinates": [-104.9951943, 39.7645187]
  };

  const sanFrancisco = {
    "type": "Point",
    "coordinates": [-122.4726194, 37.7577627]
  };

  // В каком штате находится Денвер?
  let res = await State.findOne({
    location: {
      $geoIntersects: { $geometry: denver }
    }
  });
  res.name; // Колорадо

  // В каком штате находится Сан-Франциско?
  res = await State.findOne({
    location: {
      $geoIntersects: { $geometry: sanFrancisco }
    }
  });
  res; // null
}

Итоги


GeoJSON — это не только хранение координат точек. В этом формате можно хранить пути. С использованием GeoJSON-данных можно выяснить момент попадания пользователя в геозону. А если нужно, то GeoJSON даже позволяет создавать изохроны. Вокруг формата GeoJSON сформировался набор отличных инструментов. Так, ресурс geojson.io позволяет выполнять простые визуализации координат на карте. Проект Mapbox даёт доступ к продвинутым географическим API. Пакет Turf позволяет выполнять геопространственные вычисления в браузерах и в среде Node.js.

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

Уважаемые читатели! Пользуетесь ли вы форматом GeoJSON?

RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Comments 10

    0
    Я правильно понял, полигоны с «вырезами» оно не поддерживает?
      0
      в спецификации написано, что поддерживает — первый контур внешний, остальные внутренние
        0
        Поддерживает. Для полигонов с вырезами формат такой: в массиве координат первый массив описывает внешний полигон, следующие массивы описывают вырезы. Можно посмотреть примеры в этой папке: github.com/Turfjs/turf/tree/master/packages/turf-mask/test
        0
        и не слова про Системы Координат…
          +2
          А о чем статья? Про GeoJSON? А при чем тогда node.js? И тем более почему Mongo, когда по производительности он в десятки раз уступает PostGIS?
          Начинающие картографо-разработчики, видя громкий заголовок, наверняка ожидают увидеть хоть пару абзацев про рендеринг векторных и растровых карт, про склеивание тайлов, способы отображений проекций. Я конечно понимаю, что такую задачу может взять на себя Mapbox. Я тоже очень люблю их труды. Постоянно слежу за ними в github. Но GeoJSON+Node.js — это же не про сервисы, от статьи по практике работы с картами ожидают про mapnik, про proj4js, про openlayers, про Nominatim, можно также отдельно статью про Mapbox GL JS и про оформление стилей карт.

          Что-то совсем не для новичков ваша статья. Вводные статьи на Openstreetmap и Openmaptiles хорошо раскрывают терминологию, то есть «что к чему» в карто-мании. Предлагаю автору разобраться в существующих инструментах, выбрать из них актуальные и выкатить полный список в виде статьи.
            0
            И тем более почему Mongo, когда по производительности он в десятки раз уступает PostGIS?

            А поделитесь пожалуйста материалами(ссылками) на эту тему, если располагаете таковыми. Одно время я пытался выяснить кто из них таки лучше и быстрее работает с геоданными, но ничего внятного не нашел.
              0
              Ну, что касается непосредственно производительности — это мои скромные субъективные мнения, основанные на опыте использования postgis и mongo. Mongo прекрасно и быстро выдаёт дампы со всеми данными (или заданным ограниченным количеством). Но, если задать mongo дополнительных условий, он может немного морозиться. Например, выбрать все больницы во всех городах на улице Пирогов(а|ская) или в непосредственной близости от улицы — 500м, имеющий, площадь здания не меньше 5000м2.

              С другой стороны, PostGIS имеет богатый набор функций. Минус больших баз PostGIS — в проблематичном добавлении новых данных. Добавлять данные лучше скопом, заранее отключая индексы, следственно, для бесперебойной работы требуется дополнительный сервер. Распакованный в PostGIS-БД OSM-дамп разбухает на 1.5Тб, а вместе со всеми индексами вырастает до 2.1Тб.
              0
              Справедливости ради, вряд ли имеет смысл ожидать в статье про ноду информацию о рендеринге и способах отображения проекций. Это на фронте делается
                0
                Проекции выполняются частично и на клиенте. — Но про проекции — там больше про математику, чем про средства. А вот рендеринг пока делается только на серверах. На клиенте из рендеринга — только склеивание тайлов. Даже SVG-тайлы (пусть они и расторизуются на клиенте) отрисовывать их приходится на сервере.
              0
              как бы шарить свой access token тоже не очень гуд

              Only users with full accounts can post comments. Log in, please.