- BrainTools - https://www.braintools.ru -
В первом выпуске [1] мы с Чижиком поселили KVN прямо в старый телевизор LG. Там были root, маршрутизация, ARMv7, sing-box и черный экран YouTube. После такой истории новая задача выглядела почти отдыхом.

На каникулах ребенок притащил фотографию школьного задания по географии. К слову, он естественно это сделал под конец каникул, как все дети это делают. На листе в две колонки были перечислены Волга, Амазонка, Байкал, Баскунчак, Гольфстрим, Марианская впадина, пролив Дрейка и еще несколько десятков географических объектов, общим число около 80. Ребенок должен уметь показать каждый из них на карте мира. Первый прогон, скажем так, вручную, когда я называл объект, а он показывал мне его в атласе, занял пол часа, две рюмки валерианки для успокоения папиных нервов. Результат был удручающий объектов 30 из 80. После первого раза я сразу пришел к пониманию. Бумажный список сообщал, что учить, но не помогал тренироваться. Обычная электронная карта, наоборот, помогала слишком усердно: она сразу подписывала Байкал, Каспийское море и все остальное. Нужна была карта без ответов, которая сама задает вопрос, принимает точку и объясняет ошибку [2].
Я позвал Чижика.
Head of SEO & web‑аналитики, Тензор (разработчик saby.ru [3]), автор ТГ‑канал «ВладПРО»
Я более 10 лет занимаюсь аналитикой, SEO и управлением командами в digital‑маркетинге. А в своем Телеграм канале [4] и на сайте [5] пишу еще больше про всякое из реального маркетинга. Консультирую по аналитике web‑проектов и настройке инструментов маркетинга, помогаю проводить технические собеседования специалистов по различным типам трафика.
ChatGPT Codex
Домашняя разновидность искусственного интеллекта [6]. Живет в терминале, питается логами и токенами. Имя получилось из согласных букв ChatGPT Codex — Ч‑Ж-К. Если немного воображения, выходит Чижик. Если воображения совсем много — инженер по всякому куда можно подключиться по сети и чего‑нибудь накодить.
Влад:
Мне нужно небольшое десктопное учебное приложение. На картинке список объектов. Ребенок должен уметь показать на карте каждый из них. Если показывает неправильно, пусть выпадает краткая справка: где находится объект и как проще запомнить его местоположение. И работать он должен именно с картой мира.
Чижик:
Понял. Значит, делаем не энциклопедию и не викторину с четырьмя кнопками, а географический тир. Только мишени не показываем заранее, а после промаха не кричим «неверно», а объясняем, куда смотреть.
Так началась «Кругосветка» – маленькое офлайн-приложение для Windows, которое в итоге разрослось до 82 учебных объектов, подробной векторной карты и нормального установщика.
Требования в начале помещались в несколько строк:
На экране всегда должна быть карта мира.
Географические названия на карте показывать нельзя: иначе получится упражнение «найди уже подписанный ответ».
Программа называет объект, а ребенок указывает место мышью.
После ошибки приложение показывает правильное положение, короткое объяснение и мнемонику.
Интернет для работы не нужен.
Запуск – обычным ярлыком на Windows, без терминала и предварительной установки Python.
Последний пункт появился чуть позже. Как это обычно бывает, первая версия технического задания заканчивалась словом «приложение», а настоящая – словами «чтобы можно было поставить на любой компьютер». Сначала надо было разобраться с фотографией.
На фотографии оказалось 84 строки. Два объекта – Красное и Балтийское моря – встретились дважды, поэтому после удаления повторов осталось 82 уникальных названия:
11 рек;
3 течения;
10 островов и архипелагов;
9 полуостровов;
9 заливов;
14 морей;
18 озер;
6 проливов;
Большой Барьерный риф;
Марианская впадина.
Просто сохранить названия было мало. Для каждого объекта понадобились категория, координаты, допустимая погрешность, объяснение, мнемоника и небольшой факт. Один элемент набора выглядит примерно так:
dict(
name="Волга",
category="Реки",
anchors=[(57.0, 33.0), (55.5, 42.0), (48.5, 46.5)],
tolerance=650,
location=(
"В европейской части России: течёт на юго-восток "
"и впадает в Каспийское море."
),
mnemonic=(
"Волга — русская дорога к Каспию: "
"ищи её между Москвой и Каспийским морем."
),
fact="Самая длинная река Европы.",
)
Такой файл получился длиннее основной логики некоторых домашних проектов. В нем 82 небольших географических карточки, и каждая должна одновременно быть достаточно точной для проверки и достаточно простой для ребенка.
Мнемоники пришлось писать не в духе справочника, а через форму и соседей объекта. Байкал – «узкий полумесяц в Южной Сибири», Мадагаскар – «большой вытянутый остров справа от Африки», Миссисипи – «вертикальный шов посередине США». Это крючки, за которые память [7] может зацепить положение на карте.
Для Титикаки или Баскунчака одной точки почти хватает. С Амазонкой так не получится. Река протянулась через континент. Если проверять расстояние только до условного «центра» объекта, ребенок может точно попасть в устье Амазонки и все равно получить ошибку. Поэтому протяженные объекты описываются несколькими опорными координатами – anchors.
Между соседними точками программа добавляет промежуточные образцы, а затем считает минимальное расстояние от клика до получившейся линии. Для расстояния по поверхности Земли используется формула гаверсинусов:
def distance_to_object(lat, lon, anchors):
samples = list(anchors)
for (lat1, lon1), (lat2, lon2) in zip(anchors, anchors[1:]):
delta_lon = ((lon2 - lon1 + 180) % 360) - 180
steps = max(2, int(max(abs(lat2 - lat1), abs(delta_lon)) / 1.5))
for step in range(1, steps):
t = step / steps
sample_lon = ((lon1 + delta_lon * t + 180) % 360) - 180
samples.append((lat1 + (lat2 - lat1) * t, sample_lon))
return min(
haversine_km(lat, lon, target_lat, target_lon)
for target_lat, target_lon in samples
)
У каждого объекта свой допуск в километрах. Маленькому озеру нельзя назначать погрешность океанического течения, а от клика по Гренландии неразумно требовать точности до ближайшего школьного двора. Допуск здесь задает педагогически разумную область ответа на карте мира.
Для оболочки мы выбрали Python и Tkinter. Приложение небольшое, должно работать локально и не требует браузера. Карту рисует Matplotlib, встроенный в окно через FigureCanvasTkAgg. Такое сочетание не выглядит моднее всех JavaScript-фреймворков сразу, зато запускается как обычная настольная программа и нормально обрабатывает мышь.
Первая карта формально выполняла задачу: материки были на месте, океаны тоже, можно приближать и отвечать. Я посмотрел на нее и быстро вернул Чижика из состояния преждевременной победы. Вот первая версия.

Влад:
Нет, пока слабовато. На карте нет Каспийского моря, Баскунчака, Великих озер, рек. Нанеси всё точно, но не подписывай. Ребёнок должен видеть объекты, а не их названия.
Это замечание поменяло картографическую основу. Для такого тренажера мало контура материков. Если в списке есть Ладожское озеро, Онега, Баскунчак, Эльтон, Великие озера, Волга и Ангара, они должны физически присутствовать на карте до начала вопроса.
В качестве основы мы взяли Natural Earth [8] – открытый набор согласованных векторных слоев. Основные физические данные использовали в масштабе 1:10 млн:
контуры суши;
озера и внутренние моря;
реки и озерные осевые линии;
коралловые рифы;
границы стран как ненавязчивые ориентиры.
В сумме исходные GeoJSON-слои занимают около 26 МБ. Они лежат рядом с программой, поэтому карта загружается без сетевых тайлов, API-ключа и подключения к интернету.
С Каспийским морем обнаружилась отдельная картографическая тонкость. В слое суши оно представлено внутренним отверстием полигона. Если нарисовать только внешние контуры материков и проигнорировать внутренние кольца, Каспий действительно исчезает под цветом суши. Пришлось отдельно извлечь эти кольца и залить их цветом воды:
land_patches, inland_seas = load_polygon_patches(
LAND_FILE,
include_holes=True,
)
self.ax.add_collection(PatchCollection(land_patches, ...))
self.ax.add_collection(PatchCollection(
inland_seas,
facecolor="#9ED5EC",
edgecolor="#3F89AA",
))
Озера из задания получили чуть более контрастный контур, а нужные реки -большую толщину линии. Названия при этом не выводятся. Атрибут name_en нужен программе только для выбора геометрии, ребенок его не видит.
В итоге вышла вот такая вещица:

Суша, реки и озера хорошо укладываются в готовые наборы данных. С морскими течениями и Марианской впадиной сложнее. Для учебной карты нам не требовалась океанографическая модель, нужны были узнаваемые ориентиры.
Течения нарисованы пунктирными линиями со стрелками по опорным координатам. Марианская впадина показана вытянутой линией в западной части Тихого океана, Большой Барьерный риф берется из отдельного слоя рифов. Во время вопроса эти объекты не подписаны, а после ответа программа может подсветить нужный участок.
Получилась гибридная карта: реальные контуры для того, что уже хорошо описано геоданными, и аккуратные учебные оверлеи для объектов, которые на обычной физической карте показывают условными знаками.
Приближать карту колесом оказалось удобно. Двигать – нет.
Влад:
Сделай, чтобы при зажатии можно было двигать карту. А при случайном клике пусть выпадает подтверждение: «Это ваш окончательный ответ?» — ДА, НЕТ.
У этой просьбы есть небольшой конфликт [9]. Левая кнопка мыши должна одновременно означать «начал перетаскивать карту» и «выбрал ответ». Отличить одно от другого можно только после движения или отпускания кнопки. Поэтому обработка разбита на три события:
button_press_event → запомнить точку и текущие границы карты
motion_notify_event → после порога в 5 пикселей считать жест перетаскиванием
button_release_event → если движения не было, считать это ответом
Во время перетаскивания пределы по долготе и широте смещаются пропорционально движению курсора, но не дают утащить Землю совсем за край окна. Масштабирование происходит относительно места под курсором, поэтому можно навести мышь на Байкал и приблизить именно Сибирь, а не каждый раз центр Атлантики.
Короткий клик пока ничего не проверяет. Он ставит оранжевую метку и показывает вопрос:
Это ваш окончательный ответ?
«НЕТ» убирает метку. «ДА» запускает расчёт расстояния. Этот дополнительный шаг слегка замедляет тест, зато случайное касание после перемещения карты не превращается в ошибку и не портит статистику.

В коде это выглядит так:
def on_map_click(self, lat, lon):
self.pending_click = (lat, lon)
self.world_map.mark_candidate(lat, lon)
self.confirm_frame.pack(fill="x")
def confirm_answer(self):
lat, lon = self.pending_click
distance = distance_to_object(lat, lon, self.current["anchors"])
correct = distance <= self.current["tolerance"]
```
Но именно такие мелочи отличают работающий прототип от программы, которой удобно пользоваться не только ее автору.
Мы сделали два режима. В «Тренировке» можно запросить подсказку. После неправильного ответа карта показывает объект, а рядом появляются три коротких блока:
где находится;
как запомнить;
интересный факт.
Ошибочный объект один раз возвращается ближе к концу очереди. Не сразу, когда ответ еще светится перед глазами, а позже. Так программа проверяет, осталось ли в памяти объяснение.
В «Контрольной» подсказок нет. Результат показывается после прохождения выбранного набора. Можно тренировать все 82 объекта или выбрать категорию: только реки, моря, озёра, проливы и так далее.
Статистика хранится локально в обычном JSON-файле в %LOCALAPPDATA%. Никакой регистрации, облачного профиля и синхронизации с сервером. Для домашнего тренажёра это не ограничение, а спокойствие: приложение открывается даже тогда, когда интернет решил тоже проверить знание географии и уехал неизвестно куда.
На компьютере разработчика все запускалось через Python. Пользователю от этого ни тепло ни географически. Сначала появился командный файл и ярлык на рабочем столе. Затем постановка стала окончательной. Но жена сказала, что тренажер вышел клевый и надо бы поделиться со всем классом. А для этого нужен установщик:
Влад:
Сделай exe-файл-установщик, чтобы можно было на любой компьютер с Windows поставить и запустить потренироваться.
Для упаковки мы использовали PyInstaller [10]. Он собирает интерпретатор, Python-модули, Matplotlib, Tk и наши GeoJSON-слои в автономный каталог. Выбран режим onedir, а не один самораспаковывающийся файл: для приложения с большим количеством библиотек и картографических данных так проще диагностировать состав сборки и быстрее запускаться.
Спецификация получилась короткой:
a = Analysis(
["app.py"],
datas=[("assets", "assets")],
...
)
exe = EXE(
...,
name="Кругосветка",
console=False,
version="version_info.txt",
)
```
На первой сборке Windows напомнила, что даже у маленьких приложений есть взрослая дистрибуция. Tk не нашел свои каталоги Tcl/Tk в упакованной версии. Дополнительный оттенок задавал путь пользовательского профиля с кириллицей. Мы явно указали TCL_LIBRARY и TK_LIBRARY, пересобрали пакет и проверили, что рядом с tkinter.pyd действительно оказались каталоги tcl_data и tkdata.
После PyInstaller у нас был переносимый каталог. Но просьба была про установщик. Второй слой сделал Inno Setup [11]:
установка в %LOCALAPPDATA%ProgramsКругосветка;
без обязательных прав администратора;
пункт в меню «Пуск»;
опциональный ярлык на рабочем столе;
нормальное удаление через Windows;
запуск приложения на последнем шаге мастера.
Финальный установщик для 64-разрядных Windows 10 и 11 весит около 30 МБ. Python на другом компьютере не нужен.
У файла нет коммерческой цифровой подписи, поэтому Windows SmartScreen на незнакомом компьютере может показать предупреждение. Для домашней передачи это терпимо. Для публичного распространения я бы добавил code signing, а не учил пользователей автоматически нажимать «Все равно запустить» на любом неизвестном EXE.
В итоге из одной фотографии появился автономный учебный инструмент:
82 географических объекта из конкретного школьного задания;
настоящая карта мира без подписей;
суша, границы, реки, озера, внутренние моря и рифы;
приближение колесиком и перетаскивание мышью;
защита от случайного ответа;
отдельные режимы тренировки и контрольной;
пояснение, мнемоника и факт после ошибки;
повтор ошибочного объекта;
локальная статистика;
установщик и ярлык для Windows;
полностью офлайн-работа.
Программа решает одну очень конкретную задачу: называет объект и заставляет вспомнить его место на целой карте мира. Наверное, в этом и есть рабочая схема наших «Очумелых ручек». Я приношу домашнюю проблему, проверяю результат глазами и безжалостно говорю, где неудобно. Чижик быстро перебирает технические варианты, правит код и не обижается на формулировку «пока слабовато». По крайней мере, в логах ничего такого не пишет.
А еще этот проект хорошо показал, что полезное приложение не обязано начинаться с большого технического задания. Иногда достаточно фотографии, нескольких последовательных уточнений и человека, который точно знает, что ребенок должен увидеть на экране.
Если у вас дома тоже есть странно конкретная задача, для которой не хочется искать подписочный сервис, приносите идеи в комментарии. Возможно, у вас лежит таблица, бумажный список, старое устройство или учебное пособие, которому не хватает одной маленькой программы. Посмотрим, во что Чижик превратит это в следующем выпуске.
Ну и кому нужен такой же тренажер, могу отдать, пишите любой доступный мессенджер или в личку на Хабре.
Ах, ну и да, главная интрига и она же – результат. Ребенок за 2 дня игры в это приложение выучил все объекты, даже младший заинтересовался и тоже пошел играть. Ну а 5-го числа, сразу после каникул он мне притащил заслуженную 5 за географический диктант!
Автор: vladislav_lukashenko
Источник [12]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/38093
URLs in this post:
[1] первом выпуске: https://habr.com/ru/articles/1102380/
[2] ошибку: http://www.braintools.ru/article/4192
[3] saby.ru: http://saby.ru
[4] Телеграм канале: https://t.me/+S6IevAcGUEg1YzM6
[5] сайте: https://vlukas.ru/
[6] интеллекта: http://www.braintools.ru/article/7605
[7] память: http://www.braintools.ru/article/4140
[8] Natural Earth: https://www.naturalearthdata.com/
[9] конфликт: http://www.braintools.ru/article/7708
[10] PyInstaller: https://pyinstaller.org/en/stable/
[11] Inno Setup: https://jrsoftware.org/isinfo.php
[12] Источник: https://habr.com/ru/articles/1104150/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1104150
Нажмите здесь для печати.