Карта в меню, чистка повторов, подпись разработчика
This commit is contained in:
@@ -55,7 +55,11 @@
|
||||
.fullbtn:hover{border-color:var(--accent2)}
|
||||
.fullbtn .chev{float:right;transition:.2s;color:var(--muted)}
|
||||
.fullbtn.open .chev{transform:rotate(180deg)}
|
||||
.fulltitle{display:none;margin:0;padding:14px 12px 2px;font-size:16px;font-weight:700}
|
||||
|
||||
/* кнопка карты — под блоком полного расписания, только в приложении */
|
||||
#mapBtn{display:none;margin:6px 0 6px}
|
||||
body.app #mapBtn{display:block}
|
||||
.mapbtn::before{content:"⌖ ";color:var(--accent2)}
|
||||
|
||||
/* строка заголовка: стрелка назад слева, меню справа, название ровно по центру */
|
||||
.headwrap{position:relative}
|
||||
@@ -68,6 +72,8 @@
|
||||
body.app .iconbtn,body.about .iconbtn,body.map .iconbtn{display:inline-flex}
|
||||
/* на главном экране стрелки нет, но место под неё оставляем, иначе съедет центр */
|
||||
body.app .iconbtn.back{visibility:hidden}
|
||||
/* но в справке стрелка «назад» нужна и должна быть кликабельной */
|
||||
body.app.showhelp .iconbtn.back{visibility:visible}
|
||||
/* троеточие в круглой кнопке приподнято: рамка остаётся на одной линии с названием */
|
||||
#navMenu{padding-bottom:7px}
|
||||
.iconbtn:active{border-color:var(--accent2)}
|
||||
@@ -89,10 +95,28 @@
|
||||
body.about .aboutcard{display:block}
|
||||
body.about .pickers,body.about .where,body.about .dir,
|
||||
body.about #fullTable,body.about .fullbtn{display:none}
|
||||
/* внизу «О приложении» те же даты и источник уже написаны своим текстом,
|
||||
общая сноска про расписание тут только дублирует их */
|
||||
body.about footer{display:none}
|
||||
.aboutcard h2{margin:0 0 4px;font-size:17px}
|
||||
.aboutcard .ver{color:var(--muted);font-size:14px;margin-bottom:12px}
|
||||
.aboutcard p{margin:0 0 10px;font-size:14px;line-height:1.5}
|
||||
.aboutcard p.muted{color:var(--muted);font-size:13px}
|
||||
.aboutcard p.dev{color:var(--muted);font-size:13px;text-align:center;margin:18px 0 0}
|
||||
.aboutcard h3{margin:16px 0 6px;font-size:14px;color:var(--accent2)}
|
||||
.aboutcard a{color:var(--accent2)}
|
||||
|
||||
/* «Справка» — тот же экран, что и расписание, просто другой блок */
|
||||
.helpcard{display:none;background:var(--card);border:1px solid var(--line);
|
||||
border-radius:14px;padding:16px;margin-bottom:10px}
|
||||
body.app.showhelp .helpcard{display:block}
|
||||
body.app.showhelp .pickers,body.app.showhelp .where,body.app.showhelp .dir,
|
||||
body.app.showhelp .fullbtn,body.app.showhelp #fullTable,
|
||||
body.app.showhelp #mapBtn,body.app.showhelp footer{display:none}
|
||||
.helpcard h2{margin:0 0 4px;font-size:17px}
|
||||
.helpcard h3{margin:16px 0 6px;font-size:14px;color:var(--accent2)}
|
||||
.helpcard h3:first-of-type{margin-top:12px}
|
||||
.helpcard p{margin:0 0 10px;font-size:14px;line-height:1.5}
|
||||
|
||||
/* экран «Карта»: страница целиком занята картой, шапка остаётся */
|
||||
body.map{height:100vh;height:100dvh;min-height:0;padding:24px 16px 16px;overflow:hidden}
|
||||
@@ -132,12 +156,6 @@
|
||||
.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}
|
||||
@@ -191,7 +209,8 @@
|
||||
</div>
|
||||
|
||||
<nav class="menu" id="menuBox" hidden>
|
||||
<button class="menuitem" id="mMap">Карта</button>
|
||||
<button class="menuitem" id="mMap">Карта маршрута</button>
|
||||
<button class="menuitem" id="mHelp">Справка</button>
|
||||
<button class="menuitem" id="mAbout">О приложении</button>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -212,7 +231,6 @@
|
||||
|
||||
<button class="fullbtn" id="fullBtn">Полное расписание <span class="chev">▾</span></button>
|
||||
<section class="tablecard" id="fullTable">
|
||||
<h2 class="fulltitle">Полное расписание</h2>
|
||||
<div class="days" id="dayPick">
|
||||
<button data-day="weekday" class="active">Будни</button>
|
||||
<button data-day="saturday">Суббота</button>
|
||||
@@ -227,56 +245,91 @@
|
||||
<div id="tableBody"></div>
|
||||
</section>
|
||||
|
||||
<button class="fullbtn mapbtn" id="mapBtn">Карта маршрута</button>
|
||||
|
||||
<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="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 id="mapContent">
|
||||
<div class="head" id="mapHead">Сейчас автобусов нет</div>
|
||||
<div class="rows" id="mapRows"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="helpcard" id="helpBox">
|
||||
<h2>Справка</h2>
|
||||
|
||||
<h3>Как читать расписание</h3>
|
||||
<p>Таблица и списки показывают официальное время <b>отправлений</b>.
|
||||
Время прибытия перевозчик не публикует, поэтому приложение считает его
|
||||
само: до Зашейка — как выезд с Полярных Зорь плюс 20 минут, до
|
||||
Кандалакши — плюс 50 минут.</p>
|
||||
<p>Одна остановка — исключение. Когда автобус приезжает на Полярные Зори,
|
||||
он сразу уходит дальше, поэтому время прибытия совпадает с отправлением
|
||||
следующей остановки. В Зашейке автобус задерживается, и там прибытие
|
||||
считается по времени в пути, а не по расписанию.</p>
|
||||
<p>Из-за этого интервалы в обе стороны по одной дороге отличаются
|
||||
на 5–10 минут. Так и есть в официальном расписании, это не ошибка
|
||||
приложения.</p>
|
||||
|
||||
<h3>Время прибытия в карточках</h3>
|
||||
<p>В списке рейсов под заголовком направления показано время прибытия
|
||||
в Зашеек или в Кандалакшу — серым под временем отправления. Время
|
||||
московское (МСК).</p>
|
||||
|
||||
<h3>Положение автобусов на карте</h3>
|
||||
<p>GPS у приложения нет. Положение считается по расписанию: между
|
||||
известными точками маршрута координаты интерполируются по времени рейса,
|
||||
доля пути — по длине дороги, поэтому метка идёт по трассе, а не по прямой.
|
||||
В Зашейке автобус стоит, пока не подошло время обратного выезда.</p>
|
||||
<p>Точность — до нескольких минут. В пробки реальное отставание больше,
|
||||
и метка может показывать автобус позади или впереди машины.</p>
|
||||
<p>Список под картой обновляется каждые 20 секунд: для каждого автобуса
|
||||
в пути указано направление и время прибытия на следующую остановку.</p>
|
||||
|
||||
<h3>Когда обновляется расписание</h3>
|
||||
<p>Часы, ближайшие рейсы и положение автобусов обновляются автоматически.
|
||||
Список рейсов — раз в минуту, часы — раз в секунду. В приложении нет
|
||||
кнопки обновления: перезапускать ничего не нужно.</p>
|
||||
|
||||
<h3>Где брать точные данные</h3>
|
||||
<p>Время отправления перевозчик меняет без предупреждения. Сроки
|
||||
действия расписания и телефон диспетчерской службы указаны
|
||||
в разделе «О приложении» — уточнять время лучше там.</p>
|
||||
</section>
|
||||
|
||||
<section class="aboutcard" id="aboutBox">
|
||||
<h2>О приложении</h2>
|
||||
<div class="ver" id="aboutVer"></div>
|
||||
<p>Расписание автобуса №127 по маршруту Кандалакша — Полярные Зори — Зашеек.</p>
|
||||
<p>Ближайшие рейсы считаются по московскому времени и обновляются каждые 30 секунд.
|
||||
Полное расписание раскрывается прямо на этом экране — кнопкой под списком рейсов,
|
||||
с переключением между буднями, субботой и воскресеньем.</p>
|
||||
<p>В меню есть «Карта»: там видно, где сейчас едут автобусы, и как пройден
|
||||
участок между остановками. Положение считается по расписанию — приложение
|
||||
не получает данные с GPS, поэтому это оценка с точностью до нескольких минут.</p>
|
||||
<p class="muted">Расписание сверено с официальным: отправления взяты из данных
|
||||
диспетчерской службы перевозчика, летнее расписание
|
||||
с 01.05.2026 по 31.10.2026. Время московское (МСК).</p>
|
||||
<p class="muted">Карта и тайлы — VK Карты, геометрия дороги — OpenStreetMap.
|
||||
Пуш-уведомления приходят через RuStore Push — для этого нужен
|
||||
<p>Расписание автобуса №127 по маршруту Кандалакша — Полярные Зори — Зашеек.
|
||||
Ближайшие рейсы по вашей остановке, полное расписание по будням, субботам
|
||||
и воскресеньям, а также карта с положением автобусов в пути.</p>
|
||||
<p>Всё время — московское (МСК). Данные обновляются автоматически,
|
||||
интернет нужен только для карты.</p>
|
||||
|
||||
<h3>Расписание</h3>
|
||||
<p>Действует летнее расписание <b>с 1 мая по 31 октября 2026 года</b>.
|
||||
Отправления взяты из данных диспетчерской службы перевозчика,
|
||||
время прибытия приложение рассчитывает само — подробно в «Справке».</p>
|
||||
<p>Источник: <a href="https://kandalaksha.org/static/auto.html" target="_blank" rel="noopener">kandalaksha.org/static/auto.html</a><br>
|
||||
Телефон диспетчерской службы: 8(921) 174-26-00, 8(911) 303-63-28</p>
|
||||
|
||||
<h3>Что внутри</h3>
|
||||
<p>Карта и тайлы — VK Карты, геометрия дороги — OpenStreetMap.
|
||||
Пуш-уведомления приходят через RuStore Push: для этого нужен
|
||||
установленный и авторизованный RuStore на устройстве.</p>
|
||||
<p class="muted">Приложение не использует геолокацию и не передаёт
|
||||
данные о вашем местоположении: положение автобусов вычисляется
|
||||
по расписанию на устройстве.</p>
|
||||
<p class="dev">Разработчик © UA1ZBE</p>
|
||||
</section>
|
||||
|
||||
<footer>Летнее расписание с 01.05.2026 по 31.10.2026 · отправления по данным
|
||||
@@ -560,13 +613,47 @@ if (NATIVE) {
|
||||
e.stopPropagation();
|
||||
menuBox.hidden = !menuBox.hidden;
|
||||
});
|
||||
|
||||
// «Справка» — блок на этом же экране. Снимаем её и кнопкой «‹»,
|
||||
// и аппаратной «назад»: для этого кладём в историю браузера состояние,
|
||||
// чтобы система отдала его нам, а не закрыла приложение.
|
||||
const helpOn = () => document.body.classList.contains("showhelp");
|
||||
const showHelp = on => {
|
||||
document.body.classList.toggle("showhelp", on);
|
||||
if (on) window.scrollTo(0, 0);
|
||||
};
|
||||
document.getElementById("mMap").addEventListener("click",()=>{ closeMenu(); Bus.openMap(); });
|
||||
document.getElementById("mHelp").addEventListener("click",()=>{
|
||||
closeMenu();
|
||||
showHelp(true);
|
||||
history.pushState({ help: 1 }, "");
|
||||
});
|
||||
window.addEventListener("popstate", () => { if (helpOn()) showHelp(false); });
|
||||
const hideHelp = () => {
|
||||
if (!helpOn()) return false;
|
||||
showHelp(false);
|
||||
if (history.state && history.state.help) history.back(); // убрать и из истории
|
||||
return true;
|
||||
};
|
||||
|
||||
// Один шаг «назад»: справка, потом меню. false — значит, уходить из приложения.
|
||||
const backStep = () => {
|
||||
if (hideHelp()) return true;
|
||||
if (!menuBox.hidden) { closeMenu(); return true; }
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById("navBack").addEventListener("click",()=>{
|
||||
// «назад» сначала сворачивает открытое меню и только потом уходит назад
|
||||
if (!menuBox.hidden) { closeMenu(); return; }
|
||||
// «назад» сначала закрывает справку, потом сворачивает меню,
|
||||
// и только потом уходит назад
|
||||
if (backStep()) return;
|
||||
Bus.goBack();
|
||||
});
|
||||
document.getElementById("mMap").addEventListener("click",()=>{ closeMenu(); Bus.openMap(); });
|
||||
// Системная кнопка «назад» WebView не перехватывает, поэтому Android
|
||||
// спрашивает у страницы то же самое, что и стрелка «‹» (MainActivity).
|
||||
window.__busBack = backStep;
|
||||
document.getElementById("mAbout").addEventListener("click",()=>{ closeMenu(); Bus.openAbout(); });
|
||||
document.getElementById("mapBtn").addEventListener("click",()=>Bus.openMap());
|
||||
document.addEventListener("click",e=>{
|
||||
if (!e.target.closest(".menu") && !e.target.closest("#navMenu")) closeMenu();
|
||||
});
|
||||
@@ -679,12 +766,25 @@ function pinElement(cls, num, text){
|
||||
return root;
|
||||
}
|
||||
|
||||
// Свёрнутая шторка подстраивается под содержимое, а список автобусов
|
||||
// обновляется каждые 20 секунд. Ширину/высоту слушаем и у DOM, и у окна:
|
||||
// первый срабатывает при переносе строк, второй — при смене шрифта.
|
||||
// Заполняется в initSheet, ниже по файлу.
|
||||
let setSheetHeight = null;
|
||||
const refitSheet = () => { if (setSheetHeight) setSheetHeight(null); };
|
||||
if (typeof ResizeObserver === "function") {
|
||||
const st = document.getElementById("mapContent");
|
||||
if (st) new ResizeObserver(refitSheet).observe(st);
|
||||
}
|
||||
window.addEventListener("resize", refitSheet);
|
||||
|
||||
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>`;
|
||||
refitSheet();
|
||||
return;
|
||||
}
|
||||
const many = list.length > 1;
|
||||
@@ -693,6 +793,7 @@ function renderMapStatus(list){
|
||||
? `<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("");
|
||||
refitSheet(); // строк могло стать больше или меньше
|
||||
}
|
||||
|
||||
function startMap(map, mmrgl){
|
||||
@@ -842,12 +943,24 @@ function initSheet(){
|
||||
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;
|
||||
const status = sheet.querySelector(".mapstatus");
|
||||
const content = document.getElementById("mapContent");
|
||||
const max = () => Math.max(60, Math.round(box.clientHeight * 0.72));
|
||||
// Свёрнутое состояние — ровно по содержимому: ручка, список автобусов и
|
||||
// внутренние отступы прокручиваемого блока. Высоту берём у #mapContent,
|
||||
// а не у .mapstatus: тот растягивается флексом, и его scrollHeight при
|
||||
// раскрытой шторке равен clientHeight — из-за этого сворачивание
|
||||
// «по содержимому» заканчивалось бы раздуванием до максимума.
|
||||
const min = () => {
|
||||
const cs = getComputedStyle(status);
|
||||
const pad = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
|
||||
return Math.min(max(), Math.round(grab.getBoundingClientRect().height
|
||||
+ content.offsetHeight + pad));
|
||||
};
|
||||
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)));
|
||||
h = Math.max(min(), Math.min(max(), Math.round(next)));
|
||||
sheet.style.height = h + "px";
|
||||
open = !!expanded;
|
||||
sheet.classList.toggle("open", open);
|
||||
@@ -870,20 +983,26 @@ function initSheet(){
|
||||
moved = startY - e.clientY;
|
||||
if (Math.abs(moved) > 6) dragged = true;
|
||||
const want = startH + moved;
|
||||
set(want, want > MIN + 8);
|
||||
set(want, want > min() + 8);
|
||||
});
|
||||
grab.addEventListener("pointerup", e=>{
|
||||
release(e);
|
||||
if (!dragged) { set(open ? MIN : max(), !open); return; } // просто тап
|
||||
if (!dragged) { set(open ? min() : max(), !open); return; } // просто тап
|
||||
const expanded = h > box.clientHeight * 0.4;
|
||||
set(expanded ? max() : MIN, expanded);
|
||||
set(expanded ? max() : min(), expanded);
|
||||
});
|
||||
grab.addEventListener("pointercancel", ()=>{ set(open ? max() : MIN, open); });
|
||||
grab.addEventListener("pointercancel", ()=>{ set(open ? max() : min(), open); });
|
||||
|
||||
// Высота шторки. next === null — просто пересчитать по текущему содержимому,
|
||||
// не трогая состояние «раскрыта/свёрнута»: этим пользуется список автобусов.
|
||||
setSheetHeight = next => {
|
||||
set(next === null ? (open ? max() : min()) : next, open);
|
||||
};
|
||||
|
||||
// поворот экрана или смена высоты окна — пересчитать предел раскрытия
|
||||
window.addEventListener("resize", ()=> set(open ? max() : MIN, open));
|
||||
window.addEventListener("resize", ()=> set(open ? max() : min(), open));
|
||||
|
||||
set(MIN, false);
|
||||
set(min(), false);
|
||||
}
|
||||
|
||||
if (MODE === "map") { initMap(); initSheet(); }
|
||||
|
||||
Reference in New Issue
Block a user