14 Commits

Author SHA1 Message Date
b5821a1fc7 Версия 2.6.5 (сборка 14) 2026-09-29 12:04:52 +00:00
c639996ca8 Мост JS→Java: добавлен openMap()
index.html вызывает Bus.openMap(), но метода не было в репозитории.
Сборка из исходников давала бы приложение, где пункт меню «Карта»
ничего не делает. APK 2.6.4 собирался из локальной копии, поэтому у
пользователей дефект не проявлялся. Здесь же актуализирован комментарий
к version(), значение он по-прежнему берёт из менеджера пакетов.
2026-09-29 12:04:51 +00:00
9b2a45568e Расписание по официальным данным лета 2026, прибытие и шторка на карте
Данные диспетчерской службы ООО «Гарант-Авто» на 01.05–31.10.2026,
источник kandalaksha.org/static/auto.html. Опубликованы четыре списка
ОТПОРЯЖЕНИЙ, 13/10/6 рейсов; колонка 2 — это выезд из Зашейка,
а не прибытие, из-за чего приложение показывало нелогичные интервалы.

Расписание:
- убран рейс 08:00/08:50/09:20/09:50, которого нет в источнике;
- добавлен рейс 11:30/12:20/12:50/13:20;
- в рейсе на Кандалакшу 14:10 → 14:15;
- суббота и воскресенье на 5 минут (09:05 → 09:10 и далее по списку);
- 14:20, 14:50, 15:10 и 15:50 подтверждены источником и оставлены.

Время прибытия в карточках направлений считается, а не берётся
из таблицы: Зашеек = отправление + 20 мин, но не дольше интервала
до выезда оттуда; Кандалакша = + 50 мин.

Карта:
- нижняя шторка с ручкой: перетаскивание, тап для сворачивания,
  автопрокрутка текста, карта перестраивается при изменении размера;
- заголовок «Автобус №127» в одну строку на всех экранах, раньше
  на карте он переносился, а на остальных терял пробел перед №127;
- кнопка «‹» на карте возвращает на главный экран;
- между прибытием в Зашеек и выездом автобус стоит на остановке;
- убраны 40px лишней прокрутки страницы на вкладке карты.
2026-09-29 12:04:50 +00:00
6d3f794bb7 README: карта и поведение меню 2026-09-29 08:14:57 +00:00
b5eed3284c Версия 2.6.4 2026-09-29 08:14:56 +00:00
5cdddcc9b3 Карта: экран на VK Картах с положением автобусов по расписанию 2026-09-29 08:14:55 +00:00
36b59c2e2e Карта: экран на VK Картах с положением автобусов по расписанию 2026-09-29 08:14:54 +00:00
66688630a3 Карта: экран на VK Картах с положением автобусов по расписанию 2026-09-29 08:14:53 +00:00
9cda0ba3df Карта: экран на VK Картах с положением автобусов по расписанию 2026-09-29 08:14:51 +00:00
c2505ea8f3 Карта: экран на VK Картах с положением автобусов по расписанию 2026-09-29 08:14:50 +00:00
8daf3d01ce Карта: SDK VK Карт и геометрия маршрута (OSRM) 2026-09-29 08:14:49 +00:00
9c8d8b5db7 Карта: SDK VK Карт и геометрия маршрута (OSRM) 2026-09-29 08:14:48 +00:00
4f972d527b Карта: SDK VK Карт и геометрия маршрута (OSRM) 2026-09-29 08:14:46 +00:00
873a2b7cd7 Карта: SDK VK Карт и геометрия маршрута (OSRM) 2026-09-29 08:14:40 +00:00
12 changed files with 772 additions and 48 deletions

View File

@@ -12,9 +12,17 @@
- **Автоопределение дня недели** — будни/выходные. - **Автоопределение дня недели** — будни/выходные.
- **Полное расписание** — по кнопке раскрывается вся таблица - **Полное расписание** — по кнопке раскрывается вся таблица
(4 остановки: Кандалакша · ПЗ→Зашеек · Зашеек · ПЗ→Кандалакша). (4 остановки: Кандалакша · ПЗ→Зашеек · Зашеек · ПЗ→Кандалакша).
- **Карта с автобусами** — пункт меню «Карта» открывает VK Карты: видно
пройденный участок маршрута и автобусы прямо сейчас (без подписей остановок —
они были бы только шумом). Положение считается **по расписанию** (приложение
не берёт координаты с GPS), поэтому это оценка с точностью до нескольких
минут; внизу перечислены автобусы с направлением и временем прибытия.
Обновление каждые 20 сек.
- **Пуш-уведомления** — через RuStore Push, см. раздел ниже. - **Пуш-уведомления** — через RuStore Push, см. раздел ниже.
- **Меню** — кнопка `⋮` в шапке открывает выпадающее окошко с пунктом - **Меню** — кнопка `⋮` в шапке открывает выпадающее окошко с пунктами
«О приложении» (описание и версия сборки). «Карта» и «О приложении». Меню сворачивается после выбора пункта, по нажатию
вне его и по кнопке «‹» (которая в этом случае только закрывает меню,
а не уходит назад).
- Тёмная тема, живые часы МСК, подсветка ближайших рейсов (< 15 мин). - Тёмная тема, живые часы МСК, подсветка ближайших рейсов (< 15 мин).
## Технические данные ## Технические данные
@@ -97,6 +105,7 @@ apk-webview/ # исходники APK (WebView)
│ ├── java/ru/ua1zbe/bus127/ │ ├── java/ru/ua1zbe/bus127/
│ │ ├── MainActivity.java # главный экран: WebView + запрос разрешения │ │ ├── MainActivity.java # главный экран: WebView + запрос разрешения
│ │ ├── AboutActivity.java # окно «О приложении» │ │ ├── AboutActivity.java # окно «О приложении»
│ │ ├── MapActivity.java # экран карты (свой WebView на тот же index.html#map)
│ │ ├── Webs.java # общий WebView для всех экранов │ │ ├── Webs.java # общий WebView для всех экранов
│ │ ├── BusBridge.java # мост страницы и приложения (меню, назад, версия) │ │ ├── BusBridge.java # мост страницы и приложения (меню, назад, версия)
│ │ ├── Bus127App.java # инициализация RuStore Push SDK │ │ ├── Bus127App.java # инициализация RuStore Push SDK
@@ -104,6 +113,9 @@ apk-webview/ # исходники APK (WebView)
│ │ ├── PushIntents.java # переход по клику │ │ ├── PushIntents.java # переход по клику
│ │ └── push/PushStore.java # хранение push-токена │ │ └── push/PushStore.java # хранение push-токена
│ ├── assets/index.html # интерфейс (HTML/JS) │ ├── assets/index.html # интерфейс (HTML/JS)
│ ├── assets/route.js # геометрия маршрута OSRM + координаты остановок ([lat,lon])
│ ├── assets/mmr-gl.js # VK Maps SDK (UMD, глобаль mmrgl), BSD-3-Clause
│ ├── assets/mmr-gl.css # стили SDK
│ └── res/values/strings.xml │ └── res/values/strings.xml
└── settings.gradle, build.gradle, gradle.properties └── settings.gradle, build.gradle, gradle.properties
web/index.html # веб-версия (та же страница, для браузера) web/index.html # веб-версия (та же страница, для браузера)
@@ -122,4 +134,9 @@ cd apk-webview
## Лицензия ## Лицензия
MIT. MIT. В `app/src/main/assets/` лежит сторонний код со своей лицензией:
- `mmr-gl.js`, `mmr-gl.css` — VK Maps SDK, BSD-3-Clause,
текст в `mmr-gl.LICENSE.txt`;
- `route.js` — геометрия дороги и координаты остановок из OpenStreetMap
(ODbL), атрибуция также выводится самими VK Картами.

View File

@@ -19,8 +19,8 @@ android {
applicationId "ru.ua1zbe.bus127" applicationId "ru.ua1zbe.bus127"
minSdk 26 minSdk 26
targetSdk 34 targetSdk 34
versionCode 12 versionCode 14
versionName "2.6.3" versionName "2.6.5"
} }
buildTypes { buildTypes {

View File

@@ -40,6 +40,13 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".MapActivity"
android:exported="false"
android:label="@string/map_title"
android:parentActivityName=".MainActivity"
android:theme="@android:style/Theme.Material.NoActionBar" />
<activity <activity
android:name=".AboutActivity" android:name=".AboutActivity"
android:exported="false" android:exported="false"

View File

@@ -65,7 +65,7 @@
color:var(--txt);border-radius:10px;width:38px;height:38px; color:var(--txt);border-radius:10px;width:38px;height:38px;
font-size:22px;line-height:1;font-family:inherit;cursor:pointer; font-size:22px;line-height:1;font-family:inherit;cursor:pointer;
align-items:center;justify-content:center;padding:0} align-items:center;justify-content:center;padding:0}
body.app .iconbtn,body.about .iconbtn{display:inline-flex} body.app .iconbtn,body.about .iconbtn,body.map .iconbtn{display:inline-flex}
/* на главном экране стрелки нет, но место под неё оставляем, иначе съедет центр */ /* на главном экране стрелки нет, но место под неё оставляем, иначе съедет центр */
body.app .iconbtn.back{visibility:hidden} body.app .iconbtn.back{visibility:hidden}
/* троеточие в круглой кнопке приподнято: рамка остаётся на одной линии с названием */ /* троеточие в круглой кнопке приподнято: рамка остаётся на одной линии с названием */
@@ -94,6 +94,66 @@
.aboutcard p{margin:0 0 10px;font-size:14px;line-height:1.5} .aboutcard p{margin:0 0 10px;font-size:14px;line-height:1.5}
.aboutcard p.muted{color:var(--muted);font-size:13px} .aboutcard p.muted{color:var(--muted);font-size:13px}
/* экран «Карта»: страница целиком занята картой, шапка остаётся */
body.map{height:100vh;height:100dvh;min-height:0;padding:24px 16px 16px;overflow:hidden}
body.map .wrap{height:100%;display:flex;flex-direction:column;max-width:none}
body.map header{margin-bottom:12px}
/* заголовок всегда в одну строку, в том числе на карте */
body.map h1{font-size:20px;line-height:1.2;white-space:nowrap}
body.map .clock{font-size:18px;margin-top:6px}
body.map .day,body.map .pickers,body.map .where,body.map .dir,
body.map .fullbtn,body.map #fullTable,body.map .aboutcard,body.map footer{display:none}
body.map #mapBox{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:8px}
.mapcanvas{flex:1 1 auto;min-height:0;border:1px solid var(--line);
border-radius:14px;overflow:hidden;background:var(--card);position:relative}
.mapcanvas.loading::after{content:"Загружаю карту…";position:absolute;inset:0;
display:flex;align-items:center;justify-content:center;
color:var(--muted);font-size:14px}
/* нижняя шторка: свёрнута до ручки с заголовком, тянется вверх,
а текст внутри прокручивается отдельно от карты */
.mapsheet{flex:0 0 auto;position:relative;display:flex;flex-direction:column;
background:var(--card);border:1px solid var(--line);border-radius:14px;
overflow:hidden;box-shadow:0 -6px 20px rgba(0,0,0,.35)}
.grab{position:relative;flex:0 0 auto;display:flex;align-items:center;gap:8px;
padding:14px 14px 8px;cursor:grab;touch-action:none;user-select:none;
border-bottom:1px solid var(--line)}
.grab .bar{position:absolute;top:6px;left:50%;width:34px;height:4px;margin-left:-17px;
border-radius:2px;background:#3a4a70}
.grab .cap{flex:1 1 auto;min-width:0;font-size:12px;font-weight:700;color:var(--muted);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.grab .chev{color:var(--muted);font-size:11px;transition:transform .2s}
.mapsheet.open .grab .chev{transform:rotate(180deg)}
.mapstatus{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;
overscroll-behavior:contain;padding:11px 14px 14px}
.mapstatus .head{font-size:14px;font-weight:700;margin-bottom:4px}
.mapstatus .head b{color:var(--accent)}
.mapstatus .rows{font-size:13px;color:var(--muted);line-height:1.55}
.mapstatus .rows div{display:flex;gap:8px;align-items:baseline}
.mapstatus .rows .when{color:var(--txt);font-variant-numeric:tabular-nums;
white-space:nowrap;font-size:12px}
.mapstatus .hint{margin-top:7px;font-size:11px;color:var(--muted);line-height:1.4}
.note{margin-top:10px;padding-top:10px;border-top:1px solid var(--line);
font-size:12px;line-height:1.55;color:var(--muted)}
.note p{margin:0 0 8px}
.note p:last-child{margin-bottom:0}
.note b{color:var(--txt)}
/* метки на карте. Корень двигает библиотека, вид задаёт внутренний блок */
.pin{transform:translate(-50%,-100%);white-space:nowrap;pointer-events:none}
.pin .box{display:flex;align-items:center;gap:6px;padding:4px 9px 4px 5px;
border-radius:999px;background:rgba(13,18,32,.92);border:1px solid var(--line);
box-shadow:0 4px 14px rgba(0,0,0,.5)}
.pin .num{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;
justify-content:center;font-size:10px;font-weight:700;color:#04101c;flex:0 0 auto}
.pin .txt{font-size:11px;font-weight:600;color:var(--txt)}
.pin.fwd .num{background:var(--accent)}
.pin.back .num{background:var(--accent2)}
.pin::after{content:"";position:absolute;left:50%;bottom:-5px;width:8px;height:8px;
margin-left:-4px;background:rgba(13,18,32,.92);
border-right:1px solid var(--line);border-bottom:1px solid var(--line);
transform:rotate(45deg)}
.tablecard{background:var(--card);border:1px solid var(--line);border-radius:14px; .tablecard{background:var(--card);border:1px solid var(--line);border-radius:14px;
overflow:hidden;display:none;margin-bottom:8px} overflow:hidden;display:none;margin-bottom:8px}
/* переключатель типа дня в полном расписании */ /* переключатель типа дня в полном расписании */
@@ -131,6 +191,7 @@
</div> </div>
<nav class="menu" id="menuBox" hidden> <nav class="menu" id="menuBox" hidden>
<button class="menuitem" id="mMap">Карта</button>
<button class="menuitem" id="mAbout">О приложении</button> <button class="menuitem" id="mAbout">О приложении</button>
</nav> </nav>
</div> </div>
@@ -159,13 +220,47 @@
</div> </div>
<div class="thead"> <div class="thead">
<div><b>Кандалакша</b>отпр.</div> <div><b>Кандалакша</b>отпр.</div>
<div><b>Полярные Зори</b>→ Зашеек</div> <div><b>Полярные Зори</b>отпр. в Зашеек</div>
<div><b>Зашеек</b>прибытие</div> <div><b>Зашеек</b>отпр.</div>
<div><b>Полярные Зори</b>→ Кандалакша</div> <div><b>Полярные Зори</b>отпр. в Канд.</div>
</div> </div>
<div id="tableBody"></div> <div id="tableBody"></div>
</section> </section>
<section class="mapcard" id="mapBox" hidden>
<div class="mapcanvas loading" id="mapCanvas"></div>
<div class="mapsheet" id="mapSheet">
<div class="grab" id="sheetGrab">
<span class="bar"></span>
<span class="cap" id="sheetCap">Автобусы и расписание</span>
<span class="chev">▴</span>
</div>
<div class="mapstatus">
<div class="head" id="mapHead">Карта маршрута</div>
<div class="rows" id="mapRows"></div>
<div class="hint">Положение автобуса рассчитано по расписанию, а не по GPS:
между остановками интерполируется по дороге. Точность — до нескольких минут.</div>
<div class="note">
<p><b>Как читать расписание.</b> Таблица и списки показывают официальное время
<b>отправлений</b>. Время прибытия в расписании не публикуется: оно берётся
из отправления следующей остановки, потому что автобус приезжает
на Полярные Зори и сразу уходит дальше. Исключение — Зашеек: там
прибытие посчитано как выезд из Полярных Зорь плюс 20 минут.
Получившиеся интервалы в обе стороны по одной дороге отличаются
на 5–10 минут — так и есть в официальном расписании, это не ошибка.</p>
<p><b>Положение автобусов.</b> GPS у приложения нет, координаты
интерполируются по дороге между остановками по времени рейса.
В Зашейке автобус стоит, пока не подошло время обратного выезда.
Точность — до нескольких минут, в пробки отставание больше.</p>
<p class="muted">Расписание сверено с официальным: отправления взяты из данных
диспетчерской службы перевозчика, летнее расписание
с 01.05.2026 по 31.10.2026, время московское. Карта и тайлы — VK Карты,
геометрия дороги — OpenStreetMap.</p>
</div>
</div>
</div>
</section>
<section class="aboutcard" id="aboutBox"> <section class="aboutcard" id="aboutBox">
<h2>О приложении</h2> <h2>О приложении</h2>
<div class="ver" id="aboutVer"></div> <div class="ver" id="aboutVer"></div>
@@ -173,35 +268,50 @@
<p>Ближайшие рейсы считаются по московскому времени и обновляются каждые 30 секунд. <p>Ближайшие рейсы считаются по московскому времени и обновляются каждые 30 секунд.
Полное расписание раскрывается прямо на этом экране — кнопкой под списком рейсов, Полное расписание раскрывается прямо на этом экране — кнопкой под списком рейсов,
с переключением между буднями, субботой и воскресеньем.</p> с переключением между буднями, субботой и воскресеньем.</p>
<p class="muted">Расписание действует с 30.09.2020. Время московское (МСК).</p> <p>В меню есть «Карта»: там видно, где сейчас едут автобусы, и как пройден
<p class="muted">Пуш-уведомления приходят через RuStore Push — для этого нужен участок между остановками. Положение считается по расписанию — приложение
не получает данные с GPS, поэтому это оценка с точностью до нескольких минут.</p>
<p class="muted">Расписание сверено с официальным: отправления взяты из данных
диспетчерской службы перевозчика, летнее расписание
с 01.05.2026 по 31.10.2026. Время московское (МСК).</p>
<p class="muted">Карта и тайлы — VK Карты, геометрия дороги — OpenStreetMap.
Пуш-уведомления приходят через RuStore Push — для этого нужен
установленный и авторизованный RuStore на устройстве.</p> установленный и авторизованный RuStore на устройстве.</p>
</section> </section>
<footer>Расписание действует с 30.09.2020 · время московское (МСК)</footer> <footer>Летнее расписание с 01.05.2026 по 31.10.2026 · отправления по данным
диспетчерской службы перевозчика · время московское (МСК)</footer>
</div> </div>
<script> <script>
// Расписание по типам дней недели. // Расписание по типам дней недели. Взято с официального расписания
// Колонки: отправление из Кандалакши · Полярные Зори (в Зашеек) · // (данные диспетчерской службы перевозчика ООО «Гарант-Авто»,
// прибытие в Зашеек · Полярные Зори (обратно). // kandalaksha.org/static/auto.html), летнее расписание
// с 01.05.2026 по 31.10.2026.
// //
// Будни — полная таблица маршрута. По субботе и воскресенью в репозитории // Опубликованы только ОТПРАВЛЕНИЯ, и четыре колонки склеиваются
// хранились только отправления из Полярных Зорь и прибытия в Зашеек // в один круг рейса попарно, 1:1 — проверено, что моменты строго
// (списки в app/src/main/java/ru/ua1zbe/bus127/MainActivity.kt), поэтому // возрастают в каждой строке:
// первая и четвёртая колонки восстановлены по тем же правилам, что и для // кол0 — отправление из Кандалакши
// будней, и требуют сверки с официальным расписанием: // кол1 — отправление из Полярных Зорь в Зашеек
// кол0 = кол1 − 50 мин (Кандалакша → Полярные Зори занимает ровно 50 мин) // кол2 — отправление ИЗ Зашейка (обратно в Полярные Зори)
// кол3 = кол2 + 20 мин (так разность кол3−кол2 выглядит в 8 строках из 13) // кол3 — отправление из Полярных Зорь в Кандалакшу
//
// Важно: кол2 — это выезд из Зашейка, а не прибытие в него.
// Раньше колонка была подписана как прибытие, и из-за этого один и тот же
// автобус «прибывал» и «уезжал» в Зашеек в одну минуту, а интервалы
// туда и обратно выходили разными на одной и той же дороге.
// Прибытие в Зашеек в расписании не публикуется — оно считается
// отдельно, см. arrZasheek ниже.
const SCHEDULES = { const SCHEDULES = {
weekday: [ weekday: [
["05:20","06:20","06:30","06:50"], ["05:20","06:20","06:30","06:50"],
["06:10","07:00","07:25","07:45"], ["06:10","07:00","07:25","07:45"],
["07:40","08:30","09:00","09:20"], ["07:40","08:30","09:00","09:20"],
["08:00","08:50","09:20","09:50"],
["09:45","10:35","11:05","11:25"], ["09:45","10:35","11:05","11:25"],
["11:00","11:50","12:20","12:40"], ["11:00","11:50","12:20","12:40"],
["12:30","13:20","13:50","14:10"], ["11:30","12:20","12:50","13:20"],
["12:30","13:20","13:50","14:15"],
["13:30","14:20","14:50","15:10"], ["13:30","14:20","14:50","15:10"],
["14:00","14:50","15:25","15:50"], ["14:00","14:50","15:25","15:50"],
["15:30","16:20","17:00","17:25"], ["15:30","16:20","17:00","17:25"],
@@ -211,23 +321,23 @@ const SCHEDULES = {
], ],
saturday: [ saturday: [
["06:10","07:00","07:25","07:45"], ["06:10","07:00","07:25","07:45"],
["07:30","08:20","08:45","09:05"], ["07:30","08:20","08:45","09:10"],
["08:45","09:35","10:05","10:25"], ["08:45","09:35","10:05","10:30"],
["10:10","11:00","11:30","11:50"], ["10:10","11:00","11:30","11:55"],
["12:30","13:20","13:50","14:10"], ["12:30","13:20","13:50","14:15"],
["14:00","14:50","15:45","16:05"], ["14:00","14:50","15:45","16:10"],
["15:30","16:20","16:45","17:05"], ["15:30","16:20","16:45","17:10"],
["17:00","17:50","18:20","18:40"], ["17:00","17:50","18:20","18:45"],
["18:10","19:00","19:30","19:50"], ["18:10","19:00","19:30","19:55"],
["20:20","21:10","21:40","22:00"] ["20:20","21:10","21:40","22:05"]
], ],
sunday: [ sunday: [
["06:10","07:00","07:25","07:45"], ["06:10","07:00","07:25","07:45"],
["08:45","09:35","10:05","10:25"], ["08:45","09:35","10:05","10:30"],
["12:30","13:20","13:50","14:10"], ["12:30","13:20","13:50","14:15"],
["14:00","14:50","15:25","15:45"], ["14:00","14:50","15:25","15:50"],
["16:50","17:40","18:10","18:30"], ["16:50","17:40","18:10","18:35"],
["20:20","21:10","21:40","22:00"] ["20:20","21:10","21:40","22:05"]
] ]
}; };
const DAY_KEYS = ["weekday","saturday","sunday"]; const DAY_KEYS = ["weekday","saturday","sunday"];
@@ -237,21 +347,38 @@ const TRIPS_SHOWN = 5; // сколько ближайших рейсов по
// Время в пути Полярные Зори ↔ Кандалакша. В таблице расписания его нет: // Время в пути Полярные Зори ↔ Кандалакша. В таблице расписания его нет:
// в 12 строках из 13 Кандалакша → ПЗ занимает ровно 50 минут // в 12 строках из 13 Кандалакша → ПЗ занимает ровно 50 минут
// (в первой строке 60 — по всей видимости опечатка). // (в первой строке 60 — по всей видимости опечатка в самом расписании).
const RIDE_PZ_KAND = 50; const RIDE_PZ_KAND = 50;
// arrs — список прибытий: {label, col} — время из колонки таблицы, // Полярные Зори → Зашеек. В расписании тоже нет: там опубликованы только
// {label, offset} — «время в пути» от отправления (если col не задан). // отправления. Принимаем 20 минут — но не больше, чем есть между выездом
// из Полярных Зорь и выездом обратно, иначе на первом утреннем рейсе
// (06:20 → 06:30) автобус «прибыл» бы в Зашеек уже после своего выезда.
const RIDE_PZ_ZASHEEK = 20;
// Прибытие в Зашеек — оценка, потому что официального времени прибытия
// в расписании нет. Считаем от выезда из Полярных Зорь.
function arrZasheek(row){
const t1 = toMin(row[1]), t2 = toMin(row[2]);
return t1 + Math.max(0, Math.min(RIDE_PZ_ZASHEEK, t2 - t1));
}
// arrs — список прибытий: {label, col} — время из колонки расписания,
// {label, offset} — «время в пути» от отправления (если col не задан),
// {label, arrZ:true} — оценка прибытия в Зашеек, см. arrZasheek().
// В расписании опубликованы только отправления, поэтому прибытия
// (кроме Зашейка, где есть своя оценка) берутся из отправлений следующей
// остановки: автобус приезжает на ПЗ и сразу уходит дальше.
const DIRECTIONS = { const DIRECTIONS = {
// От Кандалакши и Зашейка все рейсы идут через Полярные Зори — одна карточка // От Кандалакши и Зашейка все рейсы идут через Полярные Зори — одна карточка
"Кандалакша": [ "Кандалакша": [
{to:"Полярные Зори, Зашеек", dep:0, via:"через Полярные Зори", {to:"Полярные Зори, Зашеек", dep:0, via:"через Полярные Зори",
arrs:[{label:"Полярные Зори", col:1}, arrs:[{label:"Полярные Зори", col:1},
{label:"Зашеек", col:2}]} {label:"Зашеек", arrZ:true}]}
], ],
"Полярные Зори": [ "Полярные Зори": [
{to:"Зашеек", dep:1, via:"≈ 30 мин в пути", {to:"Зашеек", dep:1, via:"≈ 20 мин в пути",
arrs:[{label:"Зашеек", col:2}]}, arrs:[{label:"Зашеек", arrZ:true}]},
{to:"Кандалакша", dep:3, via:"≈ 50 мин в пути", {to:"Кандалакша", dep:3, via:"≈ 50 мин в пути",
arrs:[{label:"Кандалакша", offset:RIDE_PZ_KAND}]} arrs:[{label:"Кандалакша", offset:RIDE_PZ_KAND}]}
], ],
@@ -332,7 +459,9 @@ function render(){
.map((r,ri)=>{ .map((r,ri)=>{
const dep = toMin(r[d.dep]); const dep = toMin(r[d.dep]);
const arrs = d.arrs.map(a=>{ const arrs = d.arrs.map(a=>{
const base = a.col!==undefined ? toMin(r[a.col]) : dep; const base = a.col!==undefined ? toMin(r[a.col])
: a.arrZ ? arrZasheek(r)
: dep;
return {label:a.label, t:fmtHM(base + (a.offset||0))}; return {label:a.label, t:fmtHM(base + (a.offset||0))};
}); });
return {dep, arrs, ri}; return {dep, arrs, ri};
@@ -410,12 +539,14 @@ document.getElementById("fullBtn").addEventListener("click",()=>{
// Режим показа задаётся якорью в адресе: // Режим показа задаётся якорью в адресе:
// (пусто) — браузер; // (пусто) — браузер;
// #app — главный экран приложения; // #app — главный экран приложения;
// #map — карта с положением автобуса;
// #about — окно «О приложении». // #about — окно «О приложении».
// В приложении (не в браузере) страница дёргает нативный код через объект Bus. // В приложении (не в браузере) страница дёргает нативный код через объект Bus.
const MODE = location.hash.replace("#","") || "web"; const MODE = location.hash.replace("#","") || "web";
const NATIVE = MODE !== "web" && typeof Bus !== "undefined"; const NATIVE = MODE !== "web" && typeof Bus !== "undefined";
document.body.classList.add(MODE); document.body.classList.add(MODE);
document.getElementById("mapBox").hidden = MODE !== "map";
if (MODE === "about" && NATIVE) { if (MODE === "about" && NATIVE) {
// версию берём из приложения, чтобы не расходилась со сборкой // версию берём из приложения, чтобы не расходилась со сборкой
const v = document.getElementById("aboutVer"); const v = document.getElementById("aboutVer");
@@ -429,8 +560,13 @@ if (NATIVE) {
e.stopPropagation(); e.stopPropagation();
menuBox.hidden = !menuBox.hidden; menuBox.hidden = !menuBox.hidden;
}); });
document.getElementById("navBack").addEventListener("click",()=>Bus.goBack()); document.getElementById("navBack").addEventListener("click",()=>{
document.getElementById("mAbout").addEventListener("click",()=>Bus.openAbout()); // «назад» сначала сворачивает открытое меню и только потом уходит назад
if (!menuBox.hidden) { closeMenu(); return; }
Bus.goBack();
});
document.getElementById("mMap").addEventListener("click",()=>{ closeMenu(); Bus.openMap(); });
document.getElementById("mAbout").addEventListener("click",()=>{ closeMenu(); Bus.openAbout(); });
document.addEventListener("click",e=>{ document.addEventListener("click",e=>{
if (!e.target.closest(".menu") && !e.target.closest("#navMenu")) closeMenu(); if (!e.target.closest(".menu") && !e.target.closest("#navMenu")) closeMenu();
}); });
@@ -442,6 +578,316 @@ tickClock(); renderDay(); render();
setInterval(tickClock,1000); setInterval(tickClock,1000);
setInterval(render,30000); setInterval(render,30000);
setInterval(renderDay,60000); setInterval(renderDay,60000);
// ─────────────────────────── Карта ───────────────────────────
// Где едет автобус, приложение не знает — GPS у него нет. Но в расписании
// есть отправление из Кандалакши, отправление из Полярных Зорь в Зашеек,
// прибытие в Зашеек и отправление из Полярных Зорь обратно. Между соседними
// точками известно, сколько минут шёл автобус, поэтому положение внутри
// участка можно оценить. Дорога между точками лежит в route.js (OpenStreetMap),
// доля пути считается по длине ломаной — маркер идёт по трассе, а не по прямой.
//
// Координаты и геометрия дороги, стиль и тайлы — VK Карты (MMR GL JS),
// подключаются только на этом экране, ключ доступа — из кабинета RuStore.
const VK_MAP_KEY = "RS545b2f43f1096207eeb54edd762e325c17f1e0ccd5fdd234afb61cf93f7331";
const MAP_STYLE = "https://maps.vk.com/api/styles/main_style.json";
// как часто пересчитывать положение: точность всё равно ограничена расписанием
const MAP_TICK_MS = 20000;
// четыре направления дороги: туда до Зашейка и обратно. Заполняются после
// загрузки route.js — до этого карта не считает ничего.
let KAND_TO_PZ = null; // Кандалакша → Полярные Зори
let PZ_TO_ZASHEEK = null;// Полярные Зори → Зашеек
let PZ_TO_ZASHEEK_BACK = null;
let KAND_TO_PZ_BACK = null;
let MAPREF = null; // карта нужна шторке, чтобы пересчитать размер холста
// Длина ломаной с накопленными отрезками. Расстояние считаем в
// равнопромежуточной проекции — на таком масштабе это точнее градусов.
function measure(line){
const cum = [0];
for (let i=1;i<line.length;i++){
const [x1,y1] = line[i-1], [x2,y2] = line[i];
const k = Math.cos((y1+y2)/2 * Math.PI/180); // сжатие по долготе
cum.push(cum[i-1] + Math.hypot((x2-x1)*k, y2-y1));
}
return {line, cum, len: cum[cum.length-1]};
}
// Тот же путь, но пройденный в обратную сторону
function reverse(p){
return {line: p.line.slice().reverse(),
cum: p.cum.map(v=>p.len-v).reverse(), len: p.len};
}
// Точка на ломаной по доле пройденного расстояния
function pointAt(p, f){
const target = Math.max(0, Math.min(1, f)) * p.len;
let i = 1;
while (i < p.cum.length-1 && p.cum[i] < target) i++;
const seg = p.cum[i]-p.cum[i-1] || 1;
const t = (target - p.cum[i-1]) / seg;
const [x1,y1] = p.line[i-1], [x2,y2] = p.line[i];
return [x1+(x2-x1)*t, y1+(y2-y1)*t];
}
// Рейс разложен на участки круга: Кандалакша → ПЗ → Зашеек → ПЗ → Кандалакша.
// Колонка кол2 — это выезд ИЗ Зашейка, поэтому между прибытием туда
// (оценка arrZasheek) и выездом автобус стоит на остановке: отдельный
// участок hold, на котором точка не двигается. Время в пути ПЗ → Кандалакша
// в расписании не указано, берём те же 50 минут, что и в DIRECTIONS.
function makeRun(row){
const t0=toMin(row[0]), t1=toMin(row[1]), t2=toMin(row[2]), t3=toMin(row[3]);
const z = arrZasheek(row); // прибытие в Зашеек (оценка)
const back = t3 + RIDE_PZ_KAND; // прибытие в Кандалакшу
const segs = [
{a:t0, b:t1, line:KAND_TO_PZ, to:"Полярные Зори", end:t1},
{a:t1, b:z, line:PZ_TO_ZASHEEK, to:"Зашеек", end:z}
];
if (t2 > z) segs.push({a:z, b:t2, line:PZ_TO_ZASHEEK, to:"Зашеек", end:t2, hold:true});
segs.push(
{a:t2, b:t3, line:PZ_TO_ZASHEEK_BACK, to:"Полярные Зори", end:t3},
{a:t3, b:back, line:KAND_TO_PZ_BACK, to:"Кандалакша", end:back}
);
return {t0, tEnd: back, segs: segs.filter(s=>s.b > s.a)};
}
// Все рейсы, которые сейчас в пути, вместе с точкой и участком
function busesNow(){
const d = nowMs();
const T = d.hour*60 + d.minute + d.second/60;
const out = [];
SCHEDULES[todayKey()].forEach((row, idx) => {
const run = makeRun(row);
if (T < run.t0 || T > run.tEnd) return;
const seg = run.segs.find(s => T >= s.a && T <= s.b);
if (!seg) return;
// на участке стоянки в Зашейке точка не двигается — держим конец подъезда
const f = seg.hold ? 1 : (seg.b > seg.a ? (T - seg.a) / (seg.b - seg.a) : 0);
out.push({idx, seg, at: pointAt(seg.line, f)});
});
return out;
}
function pinElement(cls, num, text){
const root = document.createElement("div");
root.className = "pin " + cls;
const box = document.createElement("div");
box.className = "box";
box.innerHTML = `<span class="num">${num}</span><span class="txt">${text}</span>`;
root.appendChild(box);
return root;
}
function renderMapStatus(list){
const head = document.getElementById("mapHead");
const rows = document.getElementById("mapRows");
if (!list.length) {
head.innerHTML = "Сейчас автобусов нет";
rows.innerHTML = `<div>Первый рейс — в ${SCHEDULES[todayKey()][0][0]} из Кандалакши.</div>`;
return;
}
const many = list.length > 1;
head.innerHTML = `Сейчас в пути <b>${list.length}</b> ${many ? "автобуса" : "автобус"}`;
rows.innerHTML = list.map(b => b.seg.hold
? `<div><span class="when">Зашеек</span><span>стоит на остановке, выезд в ${fmtHM(b.seg.b)}</span></div>`
: `<div><span class="when">→ ${b.seg.to}</span><span>прибытие в ${fmtHM(b.seg.end)}</span></div>`
).join("");
}
function startMap(map, mmrgl){
const pins = new Map(); // индекс рейса → метка, чтобы не плодить новые
function update(){
const list = busesNow();
const seen = new Set();
list.forEach(b => {
seen.add(b.idx);
let p = pins.get(b.idx);
if (!p) {
const back = b.seg.to === "Кандалакша";
const marker = new mmrgl.Marker({element: pinElement(back ? "back" : "fwd", "127", "→ " + b.seg.to),
anchor: "bottom"});
// координаты обязаны быть заданы до addTo: при добавлении SDK сразу
// пересчитывает положение метки и падает на пустом LngLat
marker.setLngLat(b.at);
marker.addTo(map);
p = {marker, lastTo: null};
pins.set(b.idx, p);
}
// подпись у участка своя — обновляем, только если направление сменилось
if (p.lastTo !== b.seg.to) {
p.lastTo = b.seg.to;
const el = p.marker.getElement();
el.querySelector(".txt").textContent = "→ " + b.seg.to;
// именно classList, а не className: у метки есть классы SDK
// (mmrgl-marker, mmrgl-marker-anchor-bottom), и перезапись className
// убрала бы position:absolute — метка пропала бы с карты
const back = b.seg.to === "Кандалакша";
el.classList.toggle("back", back);
el.classList.toggle("fwd", !back);
}
p.marker.setLngLat(b.at);
});
// рейсы, закончившиеся, убираем
pins.forEach((p, idx) => {
if (!seen.has(idx)) { p.marker.remove(); pins.delete(idx); }
});
renderMapStatus(list);
}
update();
setInterval(update, MAP_TICK_MS);
document.addEventListener("visibilitychange", () => { if (!document.hidden) update(); });
}
function linkCss(href){
const l = document.createElement("link");
l.rel = "stylesheet"; l.href = href;
document.head.appendChild(l);
}
function loadScript(src){
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.onload = resolve;
s.onerror = () => reject(new Error("не загрузился " + src));
document.head.appendChild(s);
});
}
function initMap(){
const box = document.getElementById("mapCanvas");
let ready = false;
// если стиль так и не пришёл — говорим об этом прямо, а не оставляем пустой прямоугольник
const guard = setTimeout(() => {
if (ready) return;
box.classList.remove("loading");
box.innerHTML = `<div style="padding:16px;color:#8b98b8;font-size:13px;line-height:1.5">
Карта не загрузилась. Нужен интернет: стиль и тайлы VK Карт грузятся из сети.</div>`;
}, 25000);
Promise.all([loadScript("route.js"), loadScript("mmr-gl.js")])
.then(() => {
linkCss("mmr-gl.css");
const mmrgl = window.mmrgl;
mmrgl.accessToken = VK_MAP_KEY; // ключ добавится к стилю, тайлам, шрифтам
KAND_TO_PZ = measure(LEG_KP);
PZ_TO_ZASHEEK = measure(LEG_PZ);
PZ_TO_ZASHEEK_BACK = reverse(PZ_TO_ZASHEEK);
KAND_TO_PZ_BACK = reverse(KAND_TO_PZ);
const map = new mmrgl.Map({
container: box,
style: MAP_STYLE,
center: [32.479, 67.279],
zoom: 10,
attributionControl: true
});
MAPREF = map;
map.on("load", () => {
ready = true;
clearTimeout(guard);
box.classList.remove("loading");
// линия маршрута целиком: Кандалакша → ПЗ → Зашеек
map.addSource("route", {type:"geojson", data:{
type:"Feature",
geometry:{type:"LineString", coordinates: LEG_KP.concat(LEG_PZ)},
properties:{}
}});
map.addLayer({id:"route", type:"line", source:"route",
layout:{"line-cap":"round", "line-join":"round"},
paint:{"line-color":"#4f9cff", "line-width":4, "line-opacity":.9}});
// остановки на карте не рисуем: на экране они только мешали разглядеть
// сами автобусы, а куда ехать — написано в списке под картой
// весь маршрут в кадр. Метки привязаны к точке снизу и уходят вверх,
// поэтому сверху отступ больше, иначе крайние метки срезает.
const pts = [STOPS.kandalaksha, STOPS.polyarnye, STOPS.zasheek];
map.fitBounds(
[[Math.min(...pts.map(p=>p[1])), Math.min(...pts.map(p=>p[0]))],
[Math.max(...pts.map(p=>p[1])), Math.max(...pts.map(p=>p[0]))]],
{padding: {top: 84, bottom: 64, left: 56, right: 56}, duration: 0});
startMap(map, mmrgl);
// размер контейнера после отрисовки может уточниться позже
setTimeout(() => map.resize(), 300);
});
map.on("error", e => {
// не показываем шторку ошибок: на телефоне она перекрывает карту
if (e && e.error) console.error("карта:", e.error.message || e.error);
});
})
.catch(err => {
clearTimeout(guard);
box.classList.remove("loading");
box.innerHTML = `<div style="padding:16px;color:#8b98b8;font-size:13px">
Не удалось загрузить карту: ${err.message}<br>Нужен интернет.</div>`;
});
}
// Нижняя шторка карты. За ручку её тянут вверх, короткое касание — переключает.
// Внутри своя прокрутка, поэтому карта под текстом не ездит, а карта
// VK перерисовывается под изменившуюся высоту холста.
function initSheet(){
const box = document.getElementById("mapBox");
const sheet = document.getElementById("mapSheet");
const grab = document.getElementById("sheetGrab");
if (!box || !sheet || !grab) return;
const MIN = 118;
const max = () => Math.max(MIN, Math.round(box.clientHeight * 0.72));
let open = false, h = MIN, startY = 0, startH = 0, moved = 0, dragged = false;
function set(next, expanded){
h = Math.max(MIN, Math.min(max(), Math.round(next)));
sheet.style.height = h + "px";
open = !!expanded;
sheet.classList.toggle("open", open);
if (MAPREF) MAPREF.resize(); // холст карты стал ниже — пересчитать
}
// Захват указателя есть не во всех окружениях (и может не сработать при
// синтетическом событии), поэтому на него не опираемся критично.
const captured = e => { try { return grab.hasPointerCapture(e.pointerId); }
catch(_){ return true; } };
const capture = e => { try { grab.setPointerCapture(e.pointerId); } catch(_){} };
const release = e => { try { grab.releasePointerCapture(e.pointerId); } catch(_){} };
grab.addEventListener("pointerdown", e=>{
startY = e.clientY; startH = h; moved = 0; dragged = false;
capture(e);
});
grab.addEventListener("pointermove", e=>{
if (!captured(e)) return;
moved = startY - e.clientY;
if (Math.abs(moved) > 6) dragged = true;
const want = startH + moved;
set(want, want > MIN + 8);
});
grab.addEventListener("pointerup", e=>{
release(e);
if (!dragged) { set(open ? MIN : max(), !open); return; } // просто тап
const expanded = h > box.clientHeight * 0.4;
set(expanded ? max() : MIN, expanded);
});
grab.addEventListener("pointercancel", ()=>{ set(open ? max() : MIN, open); });
// поворот экрана или смена высоты окна — пересчитать предел раскрытия
window.addEventListener("resize", ()=> set(open ? max() : MIN, open));
set(MIN, false);
}
if (MODE === "map") { initMap(); initSheet(); }
</script> </script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,150 @@
Copyright (c) 2022, VK
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MMR GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from maplibre-gl-js v3.6.2 and earlier
Version v3.6.2 of maplibre-gl-js and earlier are licensed under a BSD-3-Clause license
Copyright (c) 2023, MapLibre contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MapLibre GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from mapbox-gl-js v1.13 and earlier
Version v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license
Copyright (c) 2020, Mapbox
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Mapbox GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from glfx.js
Copyright (C) 2011 by Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Contains a portion of d3-color https://github.com/d3/d3-color
Copyright 2010-2016 Mike Bostock
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the author nor the names of contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
const STOPS = {
kandalaksha: [67.15079, 32.40827],
polyarnye: [67.36592, 32.50103],
zasheek: [67.4066, 32.54991]
};
const LEG_KP = [[32.40830,67.15086],[32.40919,67.15123],[32.40530,67.15211],[32.40164,67.15592],[32.40263,67.15627],[32.40234,67.15671],[32.40365,67.16009],[32.40439,67.16077],[32.40083,67.16153],[32.39347,67.16167],[32.37834,67.16738],[32.39686,67.17485],[32.41082,67.17577],[32.41996,67.18286],[32.42404,67.18468],[32.43127,67.18609],[32.43515,67.19157],[32.43787,67.19296],[32.43467,67.19463],[32.43517,67.20077],[32.44001,67.20499],[32.44209,67.21166],[32.42869,67.22053],[32.42562,67.22192],[32.42445,67.22492],[32.41760,67.22676],[32.41025,67.23310],[32.40865,67.23699],[32.41144,67.24032],[32.42732,67.24995],[32.43369,67.25293],[32.43263,67.25523],[32.43282,67.26107],[32.44726,67.27306],[32.45055,67.27809],[32.45315,67.28023],[32.45327,67.28199],[32.45202,67.28569],[32.44925,67.29019],[32.44998,67.29238],[32.45527,67.29506],[32.46552,67.29702],[32.48543,67.30690],[32.49115,67.31659],[32.49412,67.31822],[32.49437,67.31942],[32.49295,67.32024],[32.47807,67.32475],[32.46672,67.33477],[32.46639,67.33576],[32.46686,67.33634],[32.47102,67.33898],[32.46966,67.34397],[32.46744,67.34585],[32.46606,67.34825],[32.46588,67.35182],[32.46764,67.35447],[32.47301,67.35645],[32.48066,67.36185],[32.48653,67.36436],[32.48729,67.36651],[32.48942,67.36782],[32.49137,67.36657],[32.49723,67.36592],[32.50077,67.36628],[32.50049,67.36658],[32.50172,67.36733],[32.50322,67.36675],[32.50112,67.36633],[32.50144,67.36598]];
const LEG_PZ = [[32.50144,67.36598],[32.50112,67.36633],[32.50322,67.36675],[32.50749,67.37359],[32.50798,67.37512],[32.50647,67.37756],[32.50977,67.38124],[32.51700,67.38728],[32.52852,67.39091],[32.53133,67.39241],[32.53116,67.39391],[32.52513,67.39734],[32.53544,67.39912],[32.54459,67.39914],[32.55436,67.40345],[32.55208,67.40677],[32.54915,67.40679]];

View File

@@ -25,12 +25,17 @@ public class BusBridge {
activity.runOnUiThread(activity::finish); activity.runOnUiThread(activity::finish);
} }
@JavascriptInterface
public void openMap() {
open(MapActivity.class);
}
@JavascriptInterface @JavascriptInterface
public void openAbout() { public void openAbout() {
open(AboutActivity.class); open(AboutActivity.class);
} }
/** «2.6.3 (сборка 12)» — чтобы текст не расходился со сборкой. */ /** «2.6.5 (сборка 14)» — чтобы текст не расходился со сборкой. */
@JavascriptInterface @JavascriptInterface
public String version() { public String version() {
try { try {

View File

@@ -0,0 +1,19 @@
package ru.ua1zbe.bus127;
import android.app.Activity;
import android.os.Bundle;
import android.view.WindowManager;
/**
* Карта маршрута с положением автобуса. Рисуется той же страницей расписания
* в режиме #map, поэтому шапка, меню и тема совпадают с остальными экранами.
* Сеть нужна только здесь: стиль и тайлы VK Карт грузятся извне.
*/
public class MapActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(Webs.create(this, Webs.URL_MAP));
}
}

View File

@@ -1,6 +1,9 @@
package ru.ua1zbe.bus127; package ru.ua1zbe.bus127;
import android.app.Activity; import android.app.Activity;
import android.util.Log;
import android.webkit.ConsoleMessage;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
@@ -10,6 +13,8 @@ final class Webs {
/** Главный экран: полное расписание раскрывается прямо на странице. */ /** Главный экран: полное расписание раскрывается прямо на странице. */
static final String URL_MAIN = "file:///android_asset/index.html#app"; static final String URL_MAIN = "file:///android_asset/index.html#app";
/** Карта маршрута с положением автобуса. */
static final String URL_MAP = "file:///android_asset/index.html#map";
/** Описание приложения и версия. */ /** Описание приложения и версия. */
static final String URL_ABOUT = "file:///android_asset/index.html#about"; static final String URL_ABOUT = "file:///android_asset/index.html#about";
@@ -22,7 +27,25 @@ final class Webs {
settings.setJavaScriptEnabled(true); settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true); settings.setDomStorageEnabled(true);
settings.setSupportZoom(false); settings.setSupportZoom(false);
// Страница лежит в assets и грузится по file://, а карта (#map) тянет
// стиль, тайлы и шрифты VK Карт с https. Без этого file://-страница
// не может обращаться к внешним адресам, и карта остаётся пустой.
settings.setAllowUniversalAccessFromFileURLs(true);
webView.setWebViewClient(new WebViewClient()); webView.setWebViewClient(new WebViewClient());
webView.setWebChromeClient(new WebChromeClient() {
// страница — это весь интерфейс, поэтому её ошибки и предупреждения
// полезно видеть в logcat: иначе сбой на экране выглядит как «ничего не работает»
@Override
public boolean onConsoleMessage(ConsoleMessage m) {
if (m.messageLevel() == ConsoleMessage.MessageLevel.ERROR
|| m.messageLevel() == ConsoleMessage.MessageLevel.WARNING) {
Log.w("Bus127JS", m.messageLevel() + " " + m.message()
+ " (" + m.sourceId() + ":" + m.lineNumber() + ")");
return true;
}
return false;
}
});
webView.addJavascriptInterface(new BusBridge(activity), "Bus"); webView.addJavascriptInterface(new BusBridge(activity), "Bus");
webView.loadUrl(url); webView.loadUrl(url);
return webView; return webView;

View File

@@ -4,5 +4,6 @@
<string name="push_channel_id">bus127_push</string> <string name="push_channel_id">bus127_push</string>
<string name="push_channel_name">Пуш-уведомления</string> <string name="push_channel_name">Пуш-уведомления</string>
<string name="map_title">Карта</string>
<string name="about_title">О приложении</string> <string name="about_title">О приложении</string>
</resources> </resources>