OpenStreetMaps — это Open Source продукт, в котором 9 млн человек со всего Интернета создают свободную карту мира. Также это бесплатная альтернатива Google Картам при коммерческой разработке. Главная проблема такого продукта в том, что его сложно оптимизировать, а данные могут размечаться по-разному.

Сегодня я хочу поделиться с вами опытом нашего Go-разработчика Владимира, который знает, с какими трудностями можно столкнуться при использовании OSM в создании сложных продуктов с использованием геоданных и как их обойти.

Cтатья впервые была опубликована на Tproger.

Про инструмент

OpenStreetMap, или OSM, несмотря на название, нельзя назвать картой в привычном понимании этого слова. На самом деле это база данных, которая содержит сведения о точках земной поверхности, которая заполняется по принципу Wiki — каждый зарегистрированный пользователь может внести свои изменения. В ход идут данные GPS-трекеров, панорамы улиц или, например, спутниковые снимки. В результате от каждой точке поверхности собирается большой объем данных, на основе которых можно строить карты различного назначения.

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

О задаче

Я принимаю участие в разработке системы мониторинга грузового и муниципального транспорта, которая отслеживает передвижения техники, подключенной к региональной навигационно-информационной системе (РНИС) по городу и в области, а также между регионами ЦФО.

Задача сервиса, о котором пойдет речь, — получить координаты от транспортных средств (автобусы, маршрутки, поливальные машины, грузовая и уборочная техника), определить дороги, по которым они движутся, и выяснить максимально разрешенную скорость. Если зафиксировано нарушение — сообщить об этом и записать информацию в лог, который анализируют операторы РНИС.

Если совсем просто, то наша задача состоит в определении скоростного режима на автомобильных дорогах и фиксации нарушений.

Из OpenStreetMap получаем данные о разрешенной скорости в каждой точке. Обработка осуществляется с помощью библиотеки S2 от Google, реализация на Go. Источником данных о самих машинах служат их GPS-датчики.

Проблема 1. Быстродействие

Overpass API — распространенный сервис доступа к OSM, который позволяет извлекать данные по пользовательскому запросу. Наша система работала с инстансом сервиса, развернутым в нашей сети.

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

Решение: разработали новый сервис, который мог бы заменить собой функционал Overpass API. В качестве источника остается OSM. Разрабатываемый сервис парсит данные и на их основе строит B-Tree индекс. В качестве ключа используем s2 CellId, сгенерированный для координат точки. API сервиса реализован с использованием gRPC.

Проще говоря, новый сервис выкачивает дампы базы данных OSM, не обращаясь к API, и строит поисковый B-Tree индекс.

Проблема 2. Импорт и индексирование данных

OpenStreetMap работает с данными в двух форматах — .osm и .pbf. В качестве формата для импорта мы использовали pbf, так как он более компактный, чем .osm.

Для представления данных на картах используется несколько типов элементов:

  • Точки (Node). Имеет координаты и id, опционально может иметь список тегов.

  • Пути (Way). Упорядоченная совокупность точек (от 2 до 2000), опционально также может иметь теги.

  • Теги (Tag). Основной способ описания географических данных, каждый тег представляет из себя пару ключ-значение.

  • Отношения (Relation). Используются для описания областей на карте, могут содержать теги.

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

К сожалению, сразу проиндексировать путь не получится, так как он содержит в себе не координаты точек, а их ID. То есть сервису необходимо держать в памяти отображение ID точек и через них вычислять координаты. Учитывая объёмы — для центрального федерального округа дамп данных в формате .pbf имеет размер 680 Мб, процесс индексирования становится очень ресурсоемкой операцией.

Решение: подготовим данные перед импортом, используя утилиту для работы с данными OSM — osmium. После скачивания данных, запускаем команду:

osmium add-locations-to-ways —output=./prepared-data.osm.pbf ./data.osm.pbf

Когда команда выполнится, в файле ./prepared-data.osm.pbf получим дамп данных, в котором пути вместо ID точек содержат их координаты.

Проблема 3. Разметка данных

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

Решение: для работы с s2 в golang есть библиотека geo, с её помощью будем генерировать cellId и использовать его в качестве ключа для поиска по индексу.

Кроме cellId индексируемый элемент должен будет содержать максимально допустимую скорость. Определять ее будем на этапе индексации. Сделать это можно двумя способами: по тегу «maxspeed», либо, если «maxspeed» не указан (что бывает довольно часто), по тегу «highway».

Ограничение скорости может быть указано как в виде числового значения (в км/ч, или реже для России, в милях в час), так и в формате констант, описанных в wiki OpenStreetMap.

Пример реализации:

// Максимальное разрешение
  const StorageLevel = 18
  coords := make([][]float64, 0, len(Way.Nodes))
  // формируем слайс пар точек координат
  for _, n := range Way.Nodes {
    p := n.Point()
    coords = append(coords, []float64{p.Lon(), p.Lat()})
  }
  // Формируем полигон из точек пути
  polygon := geojson.NewLineStringGeometry(coords)
  // Генерируем id для индексируемого пути
  id := uuid.New()
  // Полезные данные
  md := Metadata{
    MDMaxSpeedKey: wms.MaxSpeed,
  }
  // Создаём из точек пути регион с нужным разрешением, таким образом
  // добиваемся интерполяции с заданным разрешением
  cover := s2.RegionCoverer{
    MinLevel: 0,
    MaxLevel: StorageLevel
    MaxCells: math.MaxInt64,
  }
  cells := cover.InteriorCovering(polygon)
  // Добавляем полученные CellId в индекс
  for _, cellID := range cells {
    index.AddPoint(&IndexPoint{
      CellID:            cellID,
      Geometry: &IndexedGeometry{
        UUID:           id,
        GeometryType:   polygon.Type,
        Geometry:       polygon,
        Metadata:     md,
      }
    })
  }

***

В итоге мы получили сервис, в котором каждый запрос обрабатывается меньше одной миллисекунды. На его создание ушло примерно три недели: от старта исследования до разработки. Сейчас мы готовим prod-решение, оформление его в виде gRPC-сервиса и интегрируем в РНИС.

Если остались вопросы — предлагаю обсудить в комментариях.

Комментарии (1)


  1. freeExec
    25.10.2022 18:22

    Надо было какую-то визуализацию приложит, ибо не очень понятно что же в итоге от дорог осталось.
    И получается дорога с двумя проезжими частями, которые имеют разные скорости, у вас превращается в хекс с некой средней скоростью. Было 40 и 60, в разных направлениях, стало 50. Теперь получается что нарушений станет в 2 раза больше: там где 50 быль ехать нельзя теперь можно; а там где 60 это нормально, стало нельзя.