Pull to refresh

Как я пытался починить поиск по картам для водителей

Reading time8 min
Views3.7K
Это история про то, как я пытался решить одну странную проблему, которая мешала мне самому. Забегая вперед, скажу — получившимся решением я доволен и довел приложение до логического конца. Однако, чтобы запустить его полноценно, нужно больше ресурсов, поэтому я решил взять паузу и спросить людей, нужно ли оно кому-то еще. С этой целью (а еще чтобы просто выговориться) и пишу здесь.

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

Проблема


Довольно давно, когда по работе пришлось много ездить по незнакомым местам, я начал замечать, что стандартный поиск по любым картам сегодня абсолютно неприменим к водителям. Смотрите: вы едете за рулем по незнакомому району, и у вас стрелка бензина на нуле. Ваши действия? Если я в этот момент в машине не один, то говорю пассажиру: “а ну, поищи рядом заправку, пока я еду”. Потому что если делать это самому, то нужно совершить следующие действия:

  1. Остановиться
  2. В приложении карт ввести в поиск “бензин” (или нажать на одну из быстрых кнопок, которые сейчас некоторые карты предлагают)
  3. Приложение делает поиск и показывает тебе огромную карту с десятком заправок
  4. Ты пытаешься понять, какая же ближайшая к тебе, и нажимаешь на нее, чтобы построить маршрут

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



Ну и третий фактор: мы ведь движемся. Результат, который карты выдали, был актуален для какой-то точки, но мы уже далеко от нее. Вы обращали внимание, что даже проложенный гуглом маршрут не обновляется автоматически, если мы сместились? Только если навигация уже была запущена — тогда он перестроит.

В общем, проблема понятна. Логика, которая работает для пешеходов и их нужд, для водителей ни о чем. Мне плевать на рейтинг места и какая там кухня — мне нужно, не отвлекаясь от дороги, в реальном времени получить маршрут к ближайшей заправке, зарядке, парковке, банкомату и тд.

Идея


Попробуем теперь определить идеальный сценарий поиска. Критерий следущий:

  • интеракция короткая и понятная, чтобы не отвлекать водителя
  • прозрачная основанная на расстоянии выдача
  • обновление в реальном времени

Первое, что напрашивается, это заменить стандартный однократный поиск сканированием. То есть цепочка действий примерно такая:

  1. Запустил сканирование
  2. Едешь, поглядываешь на обновляющиеся в реальном времени результаты
  3. Когда что-то понравилось, нажал и проложил маршрут

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

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

План


Зная свою особенность растягивать проекты, на все я решил отвести себе 2 месяца — в итоге уложился в 3. В принципе приложение достаточно простое:

  1. Клиент из пары экранов (поиск, список и карта)
  2. Периодически отправляет на сервер свои координаты, радиус поиска и тип мест
  3. Сервер строит изолинию по времени (кстати она имеет свое название на английском — isochrone), делает поиск по местам и возвращает список

Звучит как легче легкого. Я уже имел некоторый предыдущий опыт в картографии (пару лет назад делал портал недвижимости, где поиск был по карте), поэтому стек на бекенде был понятен сразу:

  • импорт данных из OpenStreetMaps в Elasticsearch
  • OpenTripPlanner для построения изолиний

На клиенте же я, подумав, решил использовать новый фреймворк от гугла — Flutter. Он кроссплатформенный, довольно гибкий, и позволяет делать полноценные приложения с минимумом кода. Конечно, он сырой и непонятно какой в продакшене, но для прототипирования выглядит идеально. Надо уточнить, что я к этому моменту имел опыт нативной разработки под андроид (был тимлидом) и решил, так сказать, посмотреть врагу в лицо. Враг оказался не так уж страшен.

Реализация


Первый прототип приложения был готов очень быстро — Flutter имеет низкий порог вхождения и понятную redux-подобную философию. Декларативное описание интерфейсов, как ни странно, тоже понравилось, равно как и горячая перезагрузка (React Native, твоя карта бита). Вообще сложилось впечатление, что гугловцы учли большинство врожденных болезней предыдущих попыток. Впрочем, я понимаю людей, которым все это может не зайти — кому-то не нравится дарт, ограниченное количество виджетов, да и “визуальный дебаг”, который тут предлагают — это что-то очень сырое.

На бекенде я сделал следующее:

  1. Поставил Nominatim, залил в его базу экстракт данных OpenStreetMaps (брал тут), используя его штатную утилитку osm2pgsql. Зачем обратился к небольшому, но очень приятному опен сорс геокодеру Photon. Ранее я уже использовал его в паре проектов — он генерирует индекс Elasticsearch, импортирует туда данные из базы Nominatim и ищет по этому индексу. Нравится он мне скоростью и чистым мапингом (например, пробовал Pelias и он мне меньше понравился). Его главная проблема — старая версия эластика, но в моем случае мне функционал самого геокодера был не нужен, только данные, поэтому после импорта я с чистой душой перенес индекс в инсталляцию эластика последней версии. Кстати, почему я выбрал именно Elasticsearch? Он очень быстрый, и у него есть функция поиска координат по полигону.
  2. Полигон — он же isochrone — для меня поначалу генерировал OpenTripPlanner. Это вполне неплохой опен сорс планировщик маршрутов. Работает он следующим образом: берет такой же экстракт OpenStreetMaps и компилирует его в большой граф дорог, который как отдельный объект сохраняет на диске. При старте сервера этот граф загружается в оперативную память и все маршруты ищутся через него. Плюсы: быстро поднять, богатый функционал (например, генерирует изолинии из коробки) и неплохая скорость работы. Минусы: эта скорость работы напрямую зависит от количества оперативки, а документация на редкость отвратна. Просто чудовищная документация. Вьетнамские флешбеки.
  3. На питоне набросал небольшое апи, которое принимает тип места и радиус поиска в секундах, запрашивает у OpenTripPlanner полигон, потом по нему ищет в Elasticsearch. К каждому найденному месту запрашивает маршрут (опять у OpenTripPlanner), берет его длину и время. После чего все собранные данные красиво упаковывает и возвращает.

Обновление результатов я сделал по смещению координат девайса на 5 метров. Карта статичная — я просто использовал апи статических карт гугла (как видите, это единственное место, где корпорация все-таки пролезла в наш уютный опен сорс мирок). Первая реализация выглядела так:







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

Еще пришло в голову добавить стрелочку, которая указывала направление к каждому результату. Работало это так:

  1. Запоминаем свои предыдущие координаты
  2. При смещении прокладываем маршрут от предыдущей позиции к текущей
  3. Берем последний сегмент нашего маршрута и сравниваем с первым сегментом маршрута каждого результата. Так как они проложены по одной и той же дорожной сетке, с 99% вероятности угол между ними близок либо к 0, либо к 180.

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



К этому моменту я был достаточно доволен получившимся приложением и решил попробовать развернуть его на несколько стран. Все-таки Ирландия — государство очень маленькое, соответственно и индекс эластика, и граф дорог были небольшие. Для пробы я решил подключить соседнюю Великобританию. Она больше примерно в 4 раза и имеет гораздо более плотную дорожную сетку (особенно столица и крупные города). И тут возникла проблема.

Elasticsearch ожидаемо неплохо переварил увеличившийся индекс, а вот с OpenTripPlanner вышел полный провал. Он написан на яве и, как я сказал выше, генерирует граф дорог, чтобы после загрузить его в оперативку. Граф для Ирландии занимал 1 гигабайт, для Великобритании — уже 5. Можно, конечно, было разбивать на страны, области и даже районы, а затем перенаправлять на нужный граф в зависимости от координат пользователя. Однако это делало невозможным прокладывать маршруты между областями, а главное — никак не решало необходимости держать все эти графы в памяти. Наконец, просто компиляция каждого такого объекта занимала ОЧЕНЬ много ресурсов и длилась вечность. Для интереса я запустил на своей машине (16 гб рам) сборку графа Франции, прождал сутки и отменил.

Очевидно, что технология, которая хорошо показала себя в небольших задачах, на масштабирование совершенно не рассчитана (по крайней мере не с моими ресурсами). Так что следовало либо признать поражение, либо переползать на другую технологию. Я взял паузу на пару дней и начал изучать, какие еще опен сорс решения существуют в мире. Оказалось, их в принципе два:


Если первый написан на яве и грузит граф дорог в оперативку, то OSRM — Open Source Routing Machine — уже написан на плюсах и держит свои (не менее монструозные) промежуточные файлы на диске. Таким образом необходимость иметь огромное количество оперативной памяти сменилась на требование большого и быстрого диска. Это уже реальнее.

Финишная прямая


Спустя пару ночей ковыряния в документации весь серверный код был переведен на новое решение. Оно действительно работало, и работало неплохо. Удалось подключить несколько стран, и даже скорость поиска выросла. Общие принципы были такие же: из экстракта OpenStreetMaps компилировались промежуточные файлы под профиль “машина” (профиль это набор весов и инструкций для ребер графа — есть профили “пешком”, “велосипед” и тд). Потом эти файлы складывались в директорию, а апи OSRM уже читало их с диска. Апи кстати оказалось немаленькое — поддерживались и изолинии, и прокладка маршрута с разнообразными нюансами, была даже генерация тайлов для карты. На последнем я решил остановиться подробнее.

Вернувшись к приложению и продолжив его тестировать, я понял еще пару вещей:

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

С радостью выкинул карту гугла (ура, теперь 100% опен сорс и свои данные), упростил меню, передвинул вниз. Начал думал, что делать с карточками. И тут очень кстати подвернулся апи тайлов, о котором я упомянул выше. Он позволяет генерировать векторный тайл для заданных координат и уровня зума. Результат выдается в виде бинарного блоба типа application/x-protobuf — довольно неудобный для манипуляций тип данных. Не буду вдаваться в подробности (пришлось немного попотеть), но вкратце мои действия выглядели так:

  1. Забрать линию построенного маршрута до точки в виде polyline
  2. Polyline -> GeoJSON
  3. Получить bounding box этой фигуры
  4. Запросить все тайлы, которые захватывает этот bounding box
  5. Конвертировать данные тайлов из бинарного формата в GeoJSON
  6. Склеить тайлы, обрезать по bounding box, скомбинировать с линией маршрута, раскрасить
  7. Получившийся GeoJSON конвертировать в растровую картинку

По ходу действия были разные нюансы, например сделать отступы у bounding box или обозначить точки цветными кольцами (и сделать их радиус постоянным для всех уровней зума). Получившаяся картинка выглядела так:



Последние штрихи


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

Истекал третий месяц разработки, и надо уже было закругляться. Чем больше добавляешь, тем больше хочется, поэтому в какой-то момент нужно просто взять себя в руки и отпустить проект. Я еще немного подправил и раскрасил интерфейс, а для чувства завершения набросал логотип приложения:



и вступительную страничку:



Ну и, наконец, финальная версия приложения:









Итог


Что ж, спасибо за внимание. Надеюсь, этот поток сознания будет кому-то интересен, а может даже и полезен. На данном этапе считаю приложение готовым: оно быстрое, без особых багов и сможет работать в любой стране мира. Кстати, возможно вы заметили, что скриншоты были как с айфона, так и с андроида, поскольку благодаря Flutter приложение абсолютно одинаково работает на обеих платформах.

Тем не менее пока что я решил все заморозить — сменил работу, появились новые заботы. Спустя пару месяцев стряхнул пыль и вот решил написать ретроспективу. Интересен ваш отзыв: понравилось ли, использовали бы, что можно изменить.

P.S. Конечно же, про готовность приложения это ерунда. Оно готово как прототип — если подходить к серьезному продакшену, то нужно делать скрипты синхронизации данных с OpenStreetMaps, проверять работу на зоопарке девайсов, локализовать интерфейсы и тд. Тот же бекенд на ноде и питоне ляжет под сколько-нибудь серьезной нагрузкой.
Tags:
Hubs:
+7
Comments9

Articles