GeoPuzzle — собери мир по кусочкам
Хочу рассказать о проекте, который развивал последние пару лет. Называется он GeoPuzzle и представляет собой игру-паззл на политической карте мира. Цель — расставить кусочки-страны на свои места. Идея подсмотрена в статье «Головоломка Mercator для знатоков географии», также в детстве играл в тетрис из стран (ещё под DOS), но название программы уже не припомню. Я был настолько вдохновлён идеей, что захотел сделать полноценный продукт, интересный не только школьникам, но и знатокам географии. За развитием проекта можно наблюдать на GitHub.
Прототип
Статья «Головоломка Mercator для знатоков географии» вышла 8 февраля 2013 года, но уже через 4 месяца у меня был готов прототип, в котором были собраны полигоны всех стран мира. Чуть позже я добавил регионы России и штаты США и сделал выбор начальной позиции на карте для полигонов случайной. Процесс разработки описал у себя в блоге, исходники выложил на GitHub. И на этом всё — застрял. У меня стало значительно меньше свободного времени, мотивация пропала (какое-никакое решение было сделано), да и сложность возрастала по экспоненте. Это был pet-project, и основная задача была изучить что-то новое, так что я слегка поизвращался с технологиями. На клиенте-то, понятно, javascript (с которым мало работал тогда), за подготовку данных отвечал скрипт на ruby (опять же, новый для меня язык), а на сервере вообще был erlang (хотел попробовать что-нибудь чисто функциональное). Полный выход из зоны комфорта: было сложно напрямую работать с объектами PostGIS, боль при конвертации строк в erlang, настройка YAWS — вообще отдельная тема… На очередном этапе я понял, что просто не справлюсь со всем этим зоопарком, чтобы сделать полноценный продукт, и ушёл думать на пару лет.
GeoPuzzle
Всё это время сайт работал, туда даже заходили люди, но мне очень хотелось добавить ещё одну маленькую деталь: чтобы показывалась хоть какая-нибудь информация о только что разгаданном полигоне. Таким образом, новогодние праздники в 2017 году я планировал провести с пользой. Из-за проблем в прототипе, я решил всё переписать и сделать продукт на чём-то знакомом — Django. Там из коробки есть вещи, которые существенно упростили мне жизнь, например, админка и работа с PostGIS через ORM. Но для начала необходимо было воссоздать тот функционал, который уже работал. На это ушло всего пара вечеров, причём большая часть времени отняла загрузка данных из KML файлов. Не столько сам процесс импорта, сколько их подготовка и восстановление моих знаний о том, как с ними работать. Кстати, полигоны я в то время брал с gadm.org. Это отлично работало для стран, но вот с точностью для регионов были определённые проблемы, так что для этой проблемы я взял тайм-аут.
Например, для России:
- Россия (2) → ЮФО (3) → Краснодарский край (4) → Выселковский район (6) → ст. Выселки (8)
- Россия (2) → ЮФО (3) → Краснодарский край (4) → Краснодар (6) → Прикубанский округ (9) → Копанской (10)
- Франция (2) → Метрополия Франции (3) → регион Нормандия (4) → департамент Орн (6) → кантон Донфрон (7) → комунна Донфрон-ан-Пуаре (8) → Донфрон (9)
Территориальные единицы в разных странах называются по-своему, но для себя я вывел такое деление: Страна (2) → Регион (4) → Округ (6). Дальнейшее административное деление оставил на потом.
Развитие проекта
На тот момент приложение представляло собой просто набор HTML-страниц с минимумом CSS. Мне хотелось поскорее проверить идею, а не заморачиваться с дизайном. Идея оказалась реализуема, и пришло время сделать для неё красивую оболочку. Т.к. чувства прекрасного в UI у меня нет, то Bootstrap мне в помощь. Интерфейс какой-никакой, а появился, и даже адаптированный под мобильные устройства. Но это был лишь первый шажок по приведению фронтенда в порядок.
Каково оно учить JavaScript в 2016?! Когда код компилируется в другой диалект, шаблонизируется, склеивается, чтобы потом быть порезанным на кусочки. Мне, как бэкендщику, было страшно, но сложность клиентской части планировалась достаточно большой, что влекло за собой необходимость использования фреймворка или библиотеки. Я остановился на React по двум причинам: нужно было не SPA, а набор компонентов для разных страниц, и хотелось побыстрее увидеть результат. Но прежде чем начать программировать, следовало настроить окружение. Теперь понимаю знакомого фронтендера, который говорил, что настраивал Webpack 2 дня. Оказывается, это была не шутка.
В то время я поддался на уговоры и реализовал логику работы приложения с помощью Redux. Возможно, это и не было ошибкой, т.к. позволило быстрее войти в тему. Формальные правила позволяли мне писать код и убеждаться, что он рабочий, не заглядывая под капот. Redux с помощью своих middleware абстрагировал меня от сетевого взаимодействия, что позволило вынести проверку ответов на сервер. Да, до этого момента клиент работал сам по себе — ajax запросом вытягивал все необходимы данные и проверял ответы самостоятельно. Пользователь мог смухлевать, подглядев данные, которые приходят с сервера. К тому же при загрузке прилетали данные, необходимые только после правильного ответа. После реализации проверки через веб-сокеты процесс стал идеологически правильнее — ответ проверяет код, который недоступен клиенту. Для пользователя это выглядело по-прежнему мгновенно: отправка крайних точек полигона на сервер, проверка, входят ли они в квадрат с погрешностью, упаковка данных для инфобокса и детализированного полигона в json и передача клиенту — укладывались в ~200ms.
Узнав всю мощь javascript, трудно остановиться. Сразу появились идеи где добавить анимации, сворачивание блоков, мигание и новые варианты игр. Один из них — «Викторина», в котором по названию, флагу, гербу или столице необходимо угадать страну. Правда, в процессе тестирования выяснилось, что у одних регионов нет флагов, а у других не указана столица, поэтому некоторые страны пришлось скрыть из списка доступных. В это же время появился режим игры на физической карте мира — без границ стран, для настоящих профи.
Открытые источники данных
Сейчас в в игре ~ 50 000 полигонов, и я хочу сказать большое спасибо таким великолепным проектам, как Wikipedia и Open Street Map, без которых наполнение базы было бы просто невозможно. Принципиальным требованием было получение и обновление данных именно из открытых источников, то есть без ручного редактирования, т.к. я не хочу делать сложную логику синхронизации. В итоге у меня получилось 2 скрипта, которые умеют обновлять инфобоксы и полигоны.
Wikipedia и SPARQL
Какая самая большая база данных о странах и регионах? Wikipedia! Изначально я хотел показывать пользователям весь инфобокс, но вскоре отказался от этой идеи. Да, там были важные вещи вроде названий, флагов, столиц и прочего, но было также и много мусора (телефонный код, форма правления, ВВП…). Попробовал парсить уже собранные, но обнаружил, что они имеют разную структуру. Это оказалось катастрофой: трудозатраты на реализацию возросли многократно. Самое время было остановиться и подумать. Буквально на следующий день я узнал о существовании специального языка запросов — SPARQL. С виду он напоминает SQL — тоже декларативный, с ключевыми словами SELECT
, WHERE
, ORDER BY
, но работает абсолютно по-другому. Небольшой пример, который возвращает список государств с их столицами на английском и русском языках:
SELECT DISTINCT ?country ?capital ?row
WHERE
{
?country wdt:P31 wd:Q3624078 .
FILTER NOT EXISTS {?country wdt:P31 wd:Q3024240}
OPTIONAL { ?country wdt:P36/rdfs:label ?capital } .
BIND(lang(?capital) as ?row)
filter (?row = 'ru' || ?row = 'en')
}
ORDER BY ?capital
Выглядит дико, не правда ли?! Проверить можно здесь. Я написал даже небольшую заметку у себя в блоге, чтобы как-то структурировать свой опыт и помочь войти в тему, т.к. каких-либо подробных материалов в интернете немного. Читать такие запросы можно научиться довольно быстро, но вот чтобы написать что-нибудь осмысленное, мне потребовались целые выходные. Очень много «магии» из wd:Q3624078
и прочих атрибутов. Нужно знать, что wdt:P31
это «сущность», а wd:Q3624078
— «суверенное государство». Неизвестные начинаются с вопросительного знака, а выполнение запроса как раз и заключается в поиске таких троек фактов, которые бы удовлетворяли условиям. Например, ?country wdt:P31 wd:Q3024240
— «найти все объекты, которые являются историческими государствами»;, а потом этот же объект участвует в других тройках ?country wdt:P36/rdfs:label ?capital
— где у него берётся столица.
Где-то через неделю у меня была готова первая версия скрипта, которая загружала информацию о регионах из Википедии. И тут выяснилась ещё одна проблема — на этот раз с данными. Некоторые svg не начинались с и не распознавались браузером как валидные изображения. К счастью, исходный файл можно редактировать. В регистрациия на wikipedia.org нет ничего сложного, но вы сразу же оказываетесь в бане на сутки. Вот такая у них защита от роботов. Так что уже следующим вечером я правил XML и радовался тому, насколько это оказалось просто, а флаги и гербы появлялись на карте.
Полигоны
Если за фактами мы идём в Википедию, то за геоданными — в Open Street Map. Было бы круто поднять у себя локальную копию, научиться языку запросов к overpass, но я даже не представляю, сколько бы на это ушло времени. А ещё же надо откуда-то брать иерархию… К счастью, один добрый человек уже решил эту проблему за меня. Сервис даже предоставляет API для получения информации. Мне удалось скачать оттуда все полигоны до 6 уровня включительно (округа) и залить всё в Postgres, вышло чуть больше 2 Gb. Не обошлось без приключений — некоторые полигоны были настолько большими (например, Канада весит больше 100 Mb в зиппованном GeoJSON), что сервер либо падал, либо не отвечал. Приходилось такие моменты обходить вручную. Скачивал все дочерние и объединял их в QGIS. Кстати, это ещё один пример open source проекта, который мне сильно помог.
Проблемы с большими данными
Так вот, база с данными у меня есть, запускаю игру и… и жду… снова жду… появились! Попробовал перетащить полигон — He’s dead, Jim! Хром не смог обработать такой объём точек и свалился. Стратегия «в лоб» больше не работает, пришла пора думать. Самое очевидное — уменьшить детализацию полигонов. Эмпирически вывел формулу, которая зависит от площади фигуры — стало лучше. На рабочем компьютере алгоритм работал на лету, сервер же жёстко ограничен в ресурсах. Подключил redis, стало лучше уже на сервере. Но урезанные полигоны хороши для Drag’n'Drop`а, при установке же на правильное место границы не совпадают с теми, которые рисуют гуглокарты. Ладно, это можно обойти применив не столь агрессивную формулу для уменьшения детализации. Раз уж 2 кеша уже есть, то почему бы не попробовать закэшировать всё, что вообще возможно?! В Redis полетели инфобоксы (на двух языках), границы, по которым рассчитывается ответ, центр полигона, а также статичные страницы сайта. В итоге игра стала работать заметно быстрее, причём снялось очень много нагрузки с Postgres, который теоретически может оказаться самым узким местом. Минус — приложение не работает без Redis совсем.
Первый деплой
Вот и пришло время показывать проект друзьям для получения обратной связи. Осталась самая малость: сгенерировать sitemap.xml, добавить robots.txt, подключить метрики, добавить кнопки соц. сетей и… задеплоить! В качестве хостинга я выбрал AWS, т.к. рассчитывал укладываться в бесплатно предоставляемые ресурсы. А это очень хороший стек для начинающего проекта:
- сервер приложения (t2.micro: 1xCPU, 1 Gb RAM, 20Gb SSD)
- база данных (db.t2.micro: 1xCPU, 1 Gb RAM, 20Gb SSD)
- файлохранилище с CDN (5 Gb S3, 50 Gb трафика)
- сервер кэширования (cache.t2.micro: 1xCPU, 0,5 Gb RAM)
- Elasticsearch + Kibana (t2.small.elasticsearch: 1xCPU, 2 Gb RAM)
Это лишь список того, чем я успел воспользоваться. Попутно решил оформлять свои грабли в виде статей, но быстро сдох. Времени уходит прилично, а не понятно, нужно ли это кому-нибудь.
В итоге за год обслуживания я заплатил что-то порядка $10, да и то по глупости. Но вот пробный период подошёл к концу, и пришлось переезжать, т.к. стоимость владения всем этим хозяйством стала приближаться к нескольким сотням долларов. Посравнивал тарифы, и остановился на DigitalOcean. Пока мне хватит и машины с 2 Gb RAM на всё (сервер приложения, БД и кеш), однако статику и CDN оставил на AWS. Сейчас вот узнал, что и у DO появился CDN и хранилище за $5/mo, так что есть смысл подумать о переезде и этой части.
Передача в Open Source
Январским вечером этого года я получил письмо из датской школы. Суть его сводилась к тому, что у них есть $100, и они хотят их мне отдать. Но есть условие — исходники проекта должны быть открытыми. До этого момента я даже и не думал об Open Source. Пара вечеров ушло на обдумывание и выбор лицензии. В итоге выложил исходники на Github под лицензией GPLv3 и получил обещанные $100. Это очень сильно подняло мотивацию — мой проект действительно оказался полезен! И я ринулся к следующей цели — редактору игр. Чтобы каждый мог создавать свои паззлы. Например, «Страны-участники Второй Мировой войны», «Округи Краснодарского края», «Страны без выхода к морю»… Но для этого была нужна регистрация и примитивный личный кабинет. В итоге разработка затянулась на долгих 3 месяца. За это время я написал дерево регионов, которое подтягивало бы данные через ajax, подключил локализацию, научился сохранять гуглокарту как картинку для генерации превьюшки и выпилил redux. Да, он помог мне разобраться с данными в самом начале, но сейчас скорее мешал. Пришлось бы тащить редьюсеры для отрисовки полигонов на карте вместе с кодом, который бы обрабатывал их перемещение. К счастью, удаление привязки к глобальному стейту заняло всего пару дней, а перенос кода в локальный даже упростил приложение. Ну и конечно же это хороший опыт :)
Подключение сервисов
Оказывается, многие платные сервисы предоставляют свои услуги бесплатно для open source проектов. Перечислю лишь те, которые подключил к своему.
— Sentry. Думаю, этот сервис по отлову ошибок знаком всем. Когда я только задеплоил проект, логгирование заключалось в отправке стектрейса на почту. Это работало только для бэкенда, но хотелось также следить за багами на фронтенде. И не зря — бесплатный лимит сообщений я исчерпал буквально за 2 недели. Большинство ошибок было в недрах гугловой библиотеки карт, что на первый взгляд очень странно. В ходе расследования выяснилось, что виноват всё-таки я. Исправления длились больше месяца, но это была очень полезная практика работы с ошибками в javascript.
— crowdin.com — локализация. Я планирую сделать проект доступным для каждого. В том числе и чтобы инфобоксы показывалась на его родном языке. Заполнить их из Википедии не проблема, но для консистентность хотелось бы также иметь и интерфейс на этом же языке, а он пока переведён только на русский и английский.
— CircleCI. Ни один современный проект не обходится без CI/CD, тестов и автоматического деплоя. Я выбрал CircleCI исключительно потому, что уже работал с TravisCI когда писал библиотеку для работы с Яндекс.Диск. У меня сложилось впечатление, что он больше подходит для тестирования библиотек, т.к. в нём легко задать матрицу окружений, в которых должен тестироваться код. А вот с самими тестами у меня беда — их не так много, как хотелось бы, хотя инфраструктура уже готова.
— Coveralls. Сервис визуализации покрытия кода. Умеет также отдавать шилдик для вставки в README.md проекта.
— SonarQube. Комбайн для контроля качества кода. Проверяет код по множеству правил, считает цикломатическую сложность, следит за покрытием тестами и даже распознаёт дублирование кода! Очень интересный сервис, с которым не успел ещё полностью разобраться.
— Github боты. Пока подключён лишь Dependabot, который актуализирует зависимости.
Предлагаю в комментариях поделиться списком сервисов и ботов на своих проектах.
Баги
Разбор багов и проблем заслуживает отдельной статьи. Были и забавные, и сложные, и трудно исправимые (поэтому Чукотка всегда стоит на своём месте). В настоящее время есть один, который очень мешает пользователям. При получении ответа, полигоны удаляются и создаются заново (в библиотеке react-google-maps), и если в этот момент пользователь перетаскивал какой-нибудь, то гуглокарта продолжает считать, что процесс ещё не закончен. Выглядит это так, будто в процессе drag’n'drop полигон пропадает, и вы больше не можете схватить какой-либо другой. Можно, конечно, поставить лок на обработку ответа в процессе drag’n'drop, но это гарантированно убъёт многопользовательскую игру, над реализацией которой я сейчас и работаю. Пытался найти способ программного прервания перетаскивания, но в итоге завёл вопрос на StackOverflow и баг на Google Maps в надежде, что на него обратят внимание. А до тех пор добавил кнопочку «игра сломалась!», которая переинициализирует всю карту, но не сбрасывает результат.
А что дальше?
- Дизайн. Я признаю, что с этим совсем всё плохо. Надо нанять дизайнера и верстальщика, потому что сам с вёрсткой и макетами не дружу.
- Монетизация. Изначально я ничего не планировал. Проект посвящён базовому образованию, которое на мой взгляд должно быть доступно всем. Меня очень вдохновило письмо из датской школы, но прошёл почти год, а за это время был всего один перевод на $5. Что ж, я и не верил, что это может хотя бы окупить сервера. Однако, всё равно завёл кампанию на Patreon. В то же время, наверное, можно подумать над введением платных возможностей для преподавателей или организаций. Например, у меня есть опыт интеграции с Learning Management Systems — набор платформ, позволяющих создавать курсы, и очень популярные в Европе и США. Насколько я понимаю, хоть исходники и под GPL на Github`е, это не мешает мне как автору развивать параллельно коммерческую версию.
- Мобилки. Хочу выпустить приложение под iOS/Android. Судя по Яндекс-метрике, четверть пользователей пытаются играть с телефона или планшета, но получается это у них с трудом.
- Разработка. Вся работа ведётся на GitHub. Я хочу продолжить развивать проект, но в одиночку это делать тяжело. В планах добавить мультиплеер, сделать теги, рейтинги и фильтры в Мастерской, добавить полигоны для физической карты (горы, моря, полуостровы). Впереди ещё много всего интересного, так что одна из целей статьи — найти единомышленников. Ещё как вариант — податься в foundation, например, Python Software Foundation и раздобыть грант.
Вот что есть на данный момент. Спасибо, что дочитали до конца! Поиграть можно здесь — geopuzzle.org, а исходники посмотреть на GitHub.
Минутка заботы от НЛО
Этот материал мог вызвать противоречивые чувства, поэтому перед написанием комментария освежите в памяти кое-что важное: