Pull to refresh

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

Reading time12 min
Views3.7K
Первое, что хочется сказать — это было сложно. Гораздо сложнее, чем я думал. Я имел до этого весьма жесткий опыт выведения продуктов в релиз на работе, однако никогда не дотаскивал до продакшена персональные проекты. Они у меня все заканчивались на прототипах разной степени отвратительности, но этот вроде бы выжил. В данный момент он запущен для 80+ стран (вся Европа, Азия и Северная Америка), на обеих мобильных платформах, и в конце статьи будут ссылки на скачивание — поэтому всех заинтересовавшихся приглашаю попробовать, поломать и поругать.

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

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

Чтобы сэкономить ваше время, начну с краткого пересказа предыдущей части: там я пишу, что вместо поиска решил использовать сканирование в движении, а интерфейс приложения — максимально упростить. Вместо нелепой для водителя строки ввода добавил несколько больших кнопок для вещей, которые могут пригодиться в дороге: АЗС, зарядка, банкомат, парковка, аптека. Вместо карты сделал список, а при выборе результата открывается навигация через Apple/Google Maps. Для приложения решил использовать Flutter (заодно познакомился, что это за зверь), данные взял из OpenStreetMap. Закончил свой рассказ на том, что был готов более-менее вменяемый прототип.

Тогда все это заняло где-то 4-5 месяцев, потом начались перемены в жизни и проект ушел на второй план — да и я начал от него уставать. Еще через месяц смахнул пыль, освежил в голове написанием статейки на хабре и решил: давай будем заканчивать. Любой человек, знающий разницу между прототипом и продуктом, на этом месте грустно улыбнется.

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

Технологии


Общая архитектура


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

  1. АПИ-сервер (Python) — основной сервер-прокладка, к нему мы и обращаемся. Там не очень много логики, в основном формирование результатов для выдачи. Самый экономичный по ресурсам.
  2. Эластик-сервер (Java) — на нем крутятся Elasticsearch и Photon (опен-сорс геокодер). Они используют один и тот же индекс, в который импортирована вся планета из OpenStreetMap. Функции сервера: поиск мест по полигону и геокодер. По своей природе эластик очень быстрый и легкий, поэтому сервер тоже не очень жирный.
  3. Гео-сервер (Node) — самый тяжелый из всех. На основе Open Source Routing Machine я написал небольшое апи, и в его задачи входят все географические вычисления: прокладка маршрутов, расчет изохрон, генерация тайлов. Каждая отдельная операция не то, чтобы очень ресурсная, однако для любого поиска их нужны десятки, и это становится узким местом. В данный момент на этом сервере 16 гб оперативки, и в целом все работает за доли секунды — кроме генерации тайлов. Когда их много в очереди, ждать картинок с картами можно и несколько секунд. К счастью, на клиенте они появляются асинхронно, и это не сильно портит общей картины (надеюсь).

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



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

Динамическая изохрона


Долгое время одной из ключевых для меня проблем была неоднородная плотность результатов. Причина вполне понятна — это неоднородная плотность самой дорожной сетки и застройки на ней. В городе среднего размера в радиусе 5 минут езды может быть 2-3 банкомата, а теперь переместимся в центр мегаполиса — и для тех же 5 минут может быть и 20, и 30 результатов. Наконец прыгаем в сельскую местность и наблюдаем почти гарантированный 0 результатов, пока мы не приблизимся к городу и радиус поиска что-то захватит.

Эта проблема дает нелинейную и непредсказуемую нагрузку на сервер, а главное — довольно паршивый опыт для юзера. Добавление в опции фильтра (5 минут, 10 минут, 30 минут) в общем-то ничего не решает. В деревне по-прежнему даже 30-минутный радиус может не вернуть ничего, а в мегаполисе и 5 минут завалят тебя результатами. Плюс мы добавили лишний функционал, в кнопки которого водитель должен на ходу попадать. В общем, ерунда, нужно принципиально другое решение.

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

  1. Ставишь лимиты результатов — например не меньше 1 и не больше 20 — и стартуешь с 10 минут
  2. Делаешь поиск по местам. Пока что для нас не нужно прокладывать маршруты к ним, поэтому обходимся чисто расчетом изохроны и фильтром по полигону в эластике — обе операции очень дешевые
  3. Если количество результатов вылезает в какую-то сторону из лимитов (в нашем случае 0 или 20+), делим или умножаем время на 2 и опять делаем поиск. Если входит в лимит, то уже тогда строим маршруты, сортируем по времени пути и т.д.

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

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

Старый способ: берем 10-минутный радиус и 30 результатов
Итог: 1 запрос на изохрону + 30 запросов на маршруты = 31

Новый способ: проверяем, 30 результатов это много, делим радиус пополам, теперь получаем 10 результатов
Итог: 2 запроса на изохрону + 10 запросов на маршруты = 12




Новая логика карт


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

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

Следущей идеей было обозначать на картах направление движения стрелкой. Это было очень просто — у меня уже был вычислен вектор, и надо было всего лишь сгенерировать геометрическую фигуру стрелки. При этом в статичном положении карты продолжали показывать позицию водителя круглым маркером. Был один нюанс — надо было нормализовать размеры маркеров и стрелок для разных уровней зума. Это вроде бы несложная задача, но на ней я застрял надолго. Дело было в следующем: все символы на карте я генерировал в метрах, а за основу брал долю высоты всей карты в метрах. Оказалось, что в ходе создания карт — определения квадратных bounding box, склеивания и обрезания тайлов по ним и т.д — накапливались погрешности, и эти небольшие погрешности в итоге приводили к визуально очень разным размерам маркеров. Особенно адская была ситуация с картами маленького масштаба. Не буду вдаваться в подробности решения, однако из-за этих погрешностей логику генерации карточек пришлось перекроить кардинально. Очень сильно в этом помог turf — прекрасный набор инструментов для манипуляции с геоданными.

Со стрелкой карты были уже полезнее, однако все-таки чего-то не хватало. После живого тестирования стало понятно — все карточки повернуты севером наверх. В статике это не бросалось в глаза, но моментально становилось очевидным, когда ты садишься за руль. Водитель подсознательно ожидает, что стрелка будет всегда направлена вверх при движении. Обнаружив это, я снова сел на работу. Это опять была одна из тех задач, которые кажутся очень простыми, но ты проведешь за ней пару дней. Казалось бы — вычисляй азимут, да и поворачивай финальный GeoJSON перед растеризацией. Но был опять один нюанс — этот финальный GeoJSON был сгенерирован по прямому bounding box, и, будучи повернутым и обрезанным по нему же, обнаруживает пустые места.



На схеме выше я примерно привел свое решение. По итогу оно получилось не очень дорогим в плане ресурсов и покрывающим 99% сценариев (думаю, что где-то возле полюсов полезут баги). В целом, сервер геовычислений по-прежнему остается самым ресурсоемким участком проекта, однако теперь его карточки маршрутов кроме эстетики еще и весьма практичны. Я пробовал даже доехать до места исключительно по этим карточкам, не включая навигацию. И даже доехал.



Качество данных


Все свои данные я брал разными способами из OpenStreetMap. Как известно, сей ресурс является на 100% некоммерческим и поддерживается коллективным разумом. Это и плюс (он бесплатный и с понятной структурой), это же и минус — данные весьма неоднородные.

На высоком уровне это означает неравномерное покрытие земного шара в целом: в странах и городах с большим количеством энтузиастов описана каждая лужайка и тропинка, а в других местах есть почти пустые зоны с базовыми схематичными объектами. Обновляются данные с точно такой же неравномерностью. Тестируя свое приложение, я пару раз натыкался на новые заправки, кафе, а иногда и целые дороги, которые не успели еще нанести на карты. Жаловаться на это глупо: тот же гугл тратит астрономические бюджеты и содержит целый штат автомобилей, отвечающих за релевантность его данных. Так что здесь лучшее, что мы можем сделать, это синхронизироваться с экстрактами OpenStreetMap почаще. Ну и пожелать удачи их сообществу.

А вот на более низком уровне из-за хаотичного редактирования карт возникает целый ряд других проблем, которые вполне решаются. В основном это касается мусорных данных и дубликатов. Многообразие этого бардака поражает: одно и то же место может быть описано 3 раза по-разному, заведения не имеют названий, типы и теги проставлены неправильно и так далее. У всего этого нет какого-то единого решения, скорее необходим комплекс мер по систематизации контента. Например, у меня есть следующие условия:

Есть несколько синонимов и вариантов одного и того же тега -> описываем словари алиасов (например parking, parking_space, parking_entrance и тд).

Есть несколько мест с одним и тем же типом и одинаковыми координатами:

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

Есть несколько мест с одним и тем же типом и почти одинаковыми координатами:

  • если у всех нет названия -> скорее всего дубликаты, не будем усложнять. Слить в одну точку с усредненными координатами, у которой именем становится тип места. Человек приедет и разберется
  • название есть только у одного -> то же самое, только теперь у нас уже есть имя
  • название есть у всех и они разные -> а вот это уже кластер

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



Интерфейс и дизайн


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

Цветовая палитра


Я долго не мог понять, что делать с палитрой. Мне очень хотелось обозначить категории мест разными цветами, за исключением зеленого — его решил приберечь как акцент. Выбрал легко различаемые и сочные цвета, вроде все хорошо. Спустя какое-то время обнаружил, что синий для АЗС перекликается с синим, которым на карте обозначается положение водителя. Ничего с этим так не сделал, оставил как есть — но внутренний перфекционист недоволен.



“По пути” и “Вы рядом”


После того, как появилась логика, определяющая направление движение водителя, стало возможным разделить маршруты на “по пути” и остальные. Как я уже говорил, это определяется первым сегментом проложенного к месту маршрута: совпадает ли он с последнем сегментом маршрута водителя. Если да, то мы уже едем к этому месту. Дальше возник вопрос, как это показать в интерфейсе. Кроме изменений в карте, которые я описал выше, пришла идея плашки “По пути” (или “En route” по-английски — вроде у них это значит то же самое). Эту же плашку я переиспользую для другого сценария: когда расстояние до найденного места меньше 25 метров. Тогда не имеет смысла прокладывать маршрут, я прячу карту и пишу, что вы уже находитесь близко (“Вы рядом” / “Look around”).



Общая карта


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



Локализация


Для нормального выхода в продакшн необходима локализация. Это всегда с одной стороны очень прямая и простая работа, с другой — когда начинаешь ею заниматься, отовсюду вылезают толпы тараканов. В моем случае основной контент из OSM уже шел локализованным, так что оставались только типы мест и элементы интерфейса. За исключением нескольких затыков (долго не мог сформулировать плашку “По пути”) все было легко. Стоит заметить, что названия мест могут занимать и 2, и 3 строчки, а в экраны небольшой ширины могут и не влезать — поэтому здесь помог виджет auto_size_text, рекомендую в разумных пределах.



А вот с технической стороны оказалось не так гладко. На сегодняшней день практически единственное решение для локализации под флаттер — библиотечка Intl_translation, и она… странная. Понятно, что им приходится сидеть на двух стульях и генерировать совершенно разные форматы строчек под андроид и айфон. Однако этот подход с вынесением переводов в отдельный класс, потом прогон скриптов из консоли (!) для создания каких-то промежуточных файлов, потом возня с ними… Это все абсолютно неочевидно для новичка, а главное тяжело для сопровождения, поскольку каждая правка сопровождается ручными танцами с бубном.

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

Релиз


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

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

Ну и, как обещал, ссылки на скачивание:





Планы


И пару слов напоследок про планы. Если кому-то кроме меня пригодится эта штука и будут скачивания — у меня есть много идей по дальнейшему развитию. Вот примерный перечень того, что хотелось бы включить в релиз №2:

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

Еще дальше очень хотелось бы поддерживать Android Auto и Apple CarPlay. Я никогда не делал для них приложений, поэтому самому любопытно попробовать.

Все, всем спасибо за внимание.
Tags:
Hubs:
+12
Comments20

Articles