Карта путешествий
Личный сервис для отметки мест, где я был: страны, города и маршруты на интерактивной карте, с итогами, поездками, бегом и коллекциями. Интерфейс на русском, стиль — сепия и бумага, как у старой карты или паспорта со штампами.
Посмотреть в действии: https://echernyshevskiy.github.io/Travel-map/friends/ — версия для друзей с моими поездками, только для просмотра.
Основная версия: https://echernyshevskiy.github.io/Travel-map/ — с правкой, для владельца. У другого человека она откроется пустой (данные хранятся в браузере), и в ней можно вести свою карту.
Приложение — один HTML-файл. Оно работает без сервера и без сети, а данные остаются у владельца: в браузере и, по желанию, в его закрытом репозитории на GitHub.
Основные принципы
- Один самодостаточный файл. Код, стили, Leaflet, справочники стран, городов и морей — всё внутри
index.html; рядом лежат только то, что нужно не всем: подробные границы (borders/), шрифт флагов для Windows (fonts/) и модуль Excel (vendor/). Файл открывается и с диска, и с GitHub Pages; при запуске ничего не грузится со сторонних CDN. (Если открывать с диска, рядом с index.html должна лежать папка fonts/: без неё Chrome и Edge на Windows покажут флаги буквами.)
- Работает без сети. Режим карты «Контуры» рисуется из встроенных границ и подписывается сам, включая моря и океаны. Поиск без сети идёт по встроенному справочнику городов. Сервис-воркер хранит приложение, загруженные фрагменты карты и подробные границы. Приложение ставится на телефон как PWA.
- Данные принадлежат владельцу. Записи хранятся в
localStorage. Есть экспорт в JSON, CSV, XLSX, GPX и KML и резервная копия перед любой заменой данных. Синхронизация между устройствами идёт через собственный закрытый репозиторий GitHub, без сторонних серверов.
- Две версии из одного кода. Основная — для владельца, с правкой. Версия для друзей (
friends/) — только для просмотра: данные вшиваются в неё при сборке, скрытые записи, заметки и ссылки на документы и альбомы туда не попадают.
Что умеет
Отметки
- Три типа записей: страна, город и маршрут. Две статусные группы: «был» и «хочу».
- Маршрут создаётся двумя способами:
- по городам — линия строится по дорогам (OSRM), а без сети прямыми отрезками;
- из GPX-файлов — с дистанцией, набором высоты, профилем высот, временем и днями в пути. Разрывы трека сохраняются.
- Поля записи:
- год и месяц, при желании точные даты («12–18 мая 2023»);
- цель поездки, компания, сезон, активность;
- поездка, к которой относится запись, и порядок внутри неё;
- заметки, фото, ссылки, «избранное», флаг «Не показывать друзьям».
- Остановки маршрута прижимаются к треку. Названия чистятся от служебных слов вроде «Local Municipality».
- Отмена последнего действия, в том числе массовых правок, и журнал изменений.
Карта
- Leaflet с кластерами. Три подложки: обычная карта OpenStreetMap, рельеф OpenTopoMap и «Контуры» — офлайн, с собственными подписями.
- Слои:
- заливка стран по числу визитов и дом пунктиром;
- регионы России (84 субъекта, «был» и «проездом»);
- тепловая карта треков и исследованные квадраты — клетки сетки масштаба 14, как в Squadrats и VeloViewer.
- Подробные границы подгружаются при приближении из папки
borders/ (Natural Earth 1:10m, регионы — OSM), в два уровня детализации. Загружаются только нужные пачки.
- Ниже карты — лента лет с проигрыванием по годам и режимом «накопительно».
- Фильтры по типу, статусу, цели, компании, сезону и годам.
- Поиск по всему миру: страны с карточкой, города из справочника и, при наличии сети, из Nominatim.
- Тёмная тема, полноэкранный режим, нижние вкладки на телефоне.
Глобус
- Режим «🌍 Глобус»: шар на холсте (d3-geo, ортографическая проекция) с теми же странами, городами, маршрутами и остановками.
- Управление: вращение с инерцией, щипок и колесо для приближения. При сильном приближении глобус сам переходит на плоскую карту в той же точке.
- Карточки городов, маршрутов и стран открываются прямо на шаре.
- Двойное касание по стране приближает к ней.
- Облёт поездки: камера летит по треку, остановки загораются с подписями.
- Дуги из дома к посещённым местам.
- Регионы России по переключателю слоя, название под курсором на ПК, управление с клавиатуры.
Поездки и истории
- Страница поездки:
- обложка, остановки по порядку, мини-карты маршрутов с профилем высот;
- период и число дней;
- кнопка облёта на глобусе.
- Полноэкранный режим истории — поездка шаг за шагом.
Итоги и статистика
- Сводка на карте: страны, города, доля мира, ближайший старт.
- Статистика: топы городов и регионов, сезоны, компании и другие срезы; сверху — липкая строка разделов для быстрого перехода, «Свернуть всё».
- Моря и океаны: на каких морях вы были (город в пределах 20 км от берега; залив засчитывается своему морю), топ-5 по визитам, «Моря России» 13 из 13, все моря по океанам. Моря дают вехи («7-е море — Эгейское»), значки «Мореход», «Семь морей», «Морской волк» и строку в итогах года. На «Контурах» и глобусе «свои» моря подписаны жирнее.
- Паспорт со штампами стран, календарь путешествий, итоги года и сравнение «год к году».
- Вехи, дневник (заметки из всех записей с поиском), снимок карты со статистикой в PNG.
- Тип путешественника — шуточный портрет по данным: образ, черты, пять шкал и факты.
- Коллекции-чек-листы:
- Золотое кольцо, города-миллионники, регионы России, моря России;
- новые семь чудес света;
- Всемирное наследие ЮНЕСКО — 1273 объекта, по странам, с поиском;
- моря и океаны мира;
- столицы и страны по частям света;
- свои коллекции.
- Квиз: флаг → страна, страна ↔ столица, «Где это?» на контурной карте. Есть режим по регионам России, раунды и марафон.
Бег
- Окно «Бег»: итоги, личные рекорды на 5/10/21,1/42,2 км, стена медалей, прогрессия темпа, календарь пробежек.
- Официальные старты: время, темп, процент обогнанных участников; предстоящие старты с обратным отсчётом и прогнозом по формуле Ригеля.
Импорт, экспорт, проверка
- Импорт фото: дата и место из EXIF, в том числе из HEIC/HEIF. Снимки группируются в поездки, а записи, которые уже есть, распознаются.
- Импорт истории Google Maps: Timeline с Android, iPhone и старого Takeout. Стоянки собираются в поездки, дом пропускается.
- Экспорт: JSON (резервная копия), CSV, XLSX, GPX, KML. Кнопка «Поделиться» на телефоне.
- «Проверка данных» находит:
- дубликаты и похожие записи;
- записи без страны или месяца;
- точки маршрута не в той стране и остановки далеко от трека;
- районы и сельсоветы вместо названий посёлков, с поиском правильного места;
- битые ссылки и подсказки объединить записи в поездки.
Синхронизация и версия для друзей
- Синхронизация через GitHub:
- файл
travel-data.json хранится в закрытом репозитории, по строке на запись;
- слияние идёт по полям каждой записи: у каждого поля своё время правки, удаления помнятся отдельно;
- правки, сделанные без сети, копятся и отправляются позже, конфликт версий решается повторным слиянием;
- токен хранится только в браузере устройства.
- Версия для друзей собирается автоматически (GitHub Actions) из того же кода и свежих данных. Она только для просмотра и без приватных полей.
Безопасность и приватность
- Данные — только у владельца. На сайте записей нет: они в браузере и, по желанию, в закрытом репозитории данных. При подключении синхронизации приложение отказывается работать с публичным репозиторием. Токен GitHub хранится только в браузере устройства и не попадает ни в файлы сайта, ни в данные.
- Версия для друзей собирается без скрытых записей, заметок, ссылок на документы и альбомы; сборка останавливается, если что-то из этого просочилось.
- Защита от внедрения кода. Всё, что попадает в интерфейс, экранируется; ссылки
javascript: отбрасываются; импорт и синхронизация проверяют записи; повреждённые записи откладываются, а не ломают приложение.
- Политика безопасности содержимого (CSP). Встроенные скрипты выполняются только по хэшу (
scripts/csp.mjs пересчитывает его при каждой сборке), сетевые запросы разрешены только к Nominatim, OSRM и GitHub API.
- Модуль Excel берётся из
vendor/ этого же сайта (запасной вариант — cdnjs) с проверкой целостности (SRI): подменённый файл браузер не выполнит.
- Процессы GitHub Actions работают с минимальными правами: тесты — только чтение, сборка сайта — запись только в этот репозиторий; токен к данным — только чтение.
Как устроено
src/
index.html разметка
styles/app.css стили
app/NN-*.js разделы приложения (карта, форма, список, итоги, синхронизация, глобус…)
lib/*.js чистая логика с модульными тестами (геометрия, форматы, слияние, разбор файлов)
data/*.json справочники: страны, регионы России, города, ЮНЕСКО
main.js точка входа
borders/ подробные границы, подгружаются при приближении
scripts/ сборка версии для друзей, проверка кода, пересборка списка ЮНЕСКО
tests/ unit/ автотесты Playwright и модульные тесты Vitest
index.html, friends/ готовые файлы — их собирает GitHub Actions
- Сборка.
- Vite и
vite-plugin-singlefile собирают всё в один index.html размером около 1,7 МБ (около 570 КБ при передаче в сжатом виде).
- Плагин
appParts склеивает разделы src/app/NN-*.js по номерам в один модуль.
- Плагин
debugHook открывает внутренние имена для отладки через window.__travelMap.
- Процессы GitHub Actions.
- «Сборка сайта» собирает
index.html и friends/index.html и коммитит их. Запускается при изменении исходников, а также четыре раза в сутки, чтобы обновлять данные в версии для друзей.
- «Тесты» прогоняют ESLint, модульные тесты Vitest и браузерные Playwright (телефон и ПК).
- Обновления выходят через ветку и pull request. Перед крупными изменениями сохраняется ветка-снимок (например,
backup-v2.58).
Подробности для разработки — в DEVELOPMENT.md.
На что опирается
| Что | Зачем | Лицензия |
|---|
| Leaflet 1.9.4, Leaflet.markercluster 1.5.3 | плоская карта и кластеры (встроены в файл) | BSD-2, MIT |
| d3-geo 3.1.1 | проекция и отсечение для глобуса | ISC |
| SheetJS (xlsx) 0.18.5 | экспорт в Excel, грузится только по нажатию | Apache-2.0 |
| Natural Earth | границы стран (лёгкие и 1:10m), города мира, названия и оси морей | общественное достояние |
| OpenStreetMap | регионы России, подложка, геокодер Nominatim, маршруты OSRM | ODbL |
| hflabs/city | все города России с регионами | CC BY-SA 4.0 |
| OpenTopoMap | подложка с рельефом | CC BY-SA |
| UNESCO World Heritage Centre | список объектов всемирного наследия | данные whc.unesco.org |
| Twemoji | шрифт флагов для Windows | CC BY 4.0 |
| Vite, Vitest, Playwright, ESLint | сборка и проверки | MIT / Apache-2.0 |
Онлайн-сервисы используются только при наличии сети и с ограничением частоты: OSM, OpenTopoMap, Nominatim, OSRM и GitHub API для синхронизации. Без них приложение продолжает работать на встроенных данных.
Сильные стороны
- Автономность и долговечность. Один файл, никакого собственного сервера и подписок. Без сети работают карта, поиск городов, глобус, коллекции и статистика.
- Владение данными. Всё лежит у владельца, выгружается в открытые форматы, синхронизируется через его же GitHub.
- Глубина. Это не только отметки на карте, а целый дневник путешествий: поездки, бег и старты, итоги, коллекции, квиз, проверка качества данных.
- Приватность для друзей. Отдельная сборка без скрытых записей, заметок и личных ссылок.
- Скорость на слабых телефонах. При 4-кратном замедлении процессора приложение запускается за 2–2,5 с, кадр глобуса при вращении занимает около 30 мс.
- Проверки при каждом изменении. Около 115 модульных тестов и около 180 браузерных сценариев на телефоне и ПК. Код проверяется ESLint.
Ограничения
- Справочник без сети содержит только крупные места: города от ~100 тыс. жителей и все города России. Сёла и урочища находятся только онлайн, через OpenStreetMap.
- Синхронизация требует настройки: нужен закрытый репозиторий и токен GitHub. Данные в нём не шифруются.
- Хранилище браузера ограничено (примерно 5 МБ на
localStorage). Фото хранятся ссылками, а не файлами.
- Онлайн-сервисы бесплатные и с лимитами: Nominatim — 1 запрос в секунду, публичный OSRM — без гарантий доступности. При сбоях приложение переходит на встроенные данные.
- Каждое обновление скачивается целиком: весь файл заново, около 570 КБ в сжатом виде.
- Однопользовательский сервис. Совместного редактирования нет, а версия для друзей — только для просмотра.
- Глобус упрощён: контуры стран без рельефа и тайлов, при сильном приближении он передаёт управление плоской карте.
Интересные решения
- Глобус на холсте без WebGL.
- d3-geo не любит внешние кольца в порядке GeoJSON и заливает весь шар «наизнанку». Кольца разворачиваются, если площадь контура получается больше полусферы.
- Пока шар вращается, рисуются упрощённые контуры (Дуглас — Пекер, 0,45°).
- Невидимые страны отсекаются по центру и угловому радиусу.
- Полностью видимые контуры рисуются напрямую, без дорогого отсечения d3.
- Слияние при синхронизации по полям. Заметка, поправленная на одном устройстве, и год, поправленный на другом, не затирают друг друга. Удаления хранятся отдельно, а при ответе GitHub «409 Conflict» файл забирается заново и сливается.
- Контуры стран и регионов встроены компактно и без потерь. Вместо GeoJSON — целые числа в тысячных градуса (у микрогосударств — десятитысячных) и разности соседних точек: вдвое меньше, а при сборке проверяется, что распакованное совпадает с исходным до бита.
- Подписи на «Контурах» без тайлов.
- Точка подписи ставится в «полюс недоступности» страны (polylabel).
- Ширина надписи меряется настоящим шрифтом, подпись выводится, только если помещается в страну.
- Подписи обходят отметки и друг друга.
- Океаны, моря, заливы и проливы (281 название, ~10 КБ) подписаны как в атласе: курсивом, с разрядкой, у вытянутых морей — вдоль оси водоёма. Ось заранее считается скриптом
scripts/build-seas.mjs как самая длинная хорда через «полюс недоступности» в проекции Меркатора. По её длине на экране решается, помещается ли подпись, а запас уходит в разрядку. Те же подписи есть на глобусе.
- Остановки на линии маршрута. Точка остановки «прижимается» к ближайшему месту трека, если оно в пределах 30 км. Остановки дальше этого попадают в «Проверку данных».
- Город не на своём месте. Проверка данных сверяет каждый город и остановку с одноимённым городом справочника той же страны: если точка дальше 15–40 км (порог растёт с размером города), скорее всего, геокодер вернул центр области или штата. Одна кнопка переносит точку в центр города, «Так и задумано» — скрывает находку. Коллекции засчитывают город и по названию со страной, даже если точка сдвинута.
- «На каком море город» без сети. Контуры 281 водоёма (Natural Earth, упрощены до ~2 км, ~75 КБ в сжатом виде) встроены текстом и разбираются при первом обращении. Город ищется в воде или в 20 км от берега; соседство водоёмов (общие вершины контуров) даёт «залив → море → океан», с поправками по российской традиции (Норвежское море — Северный Ледовитый океан, Каттегат — Балтика). Ответы кэшируются по координатам.
- Коллекция ЮНЕСКО на 1273 объекта считается за миллисекунды благодаря сетке 0,5° по отметкам и точкам треков. Список встроен текстом и разбирается только при первом открытии коллекций, поэтому запуск не замедляется.