Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d3f794bb7 | |||
| b5eed3284c | |||
| 5cdddcc9b3 | |||
| 36b59c2e2e | |||
| 66688630a3 | |||
| 9cda0ba3df | |||
| c2505ea8f3 | |||
| 8daf3d01ce | |||
| 9c8d8b5db7 | |||
| 4f972d527b | |||
| 873a2b7cd7 |
23
README.md
23
README.md
@@ -12,9 +12,17 @@
|
||||
- **Автоопределение дня недели** — будни/выходные.
|
||||
- **Полное расписание** — по кнопке раскрывается вся таблица
|
||||
(4 остановки: Кандалакша · ПЗ→Зашеек · Зашеек · ПЗ→Кандалакша).
|
||||
- **Карта с автобусами** — пункт меню «Карта» открывает VK Карты: видно
|
||||
пройденный участок маршрута и автобусы прямо сейчас (без подписей остановок —
|
||||
они были бы только шумом). Положение считается **по расписанию** (приложение
|
||||
не берёт координаты с GPS), поэтому это оценка с точностью до нескольких
|
||||
минут; внизу перечислены автобусы с направлением и временем прибытия.
|
||||
Обновление каждые 20 сек.
|
||||
- **Пуш-уведомления** — через RuStore Push, см. раздел ниже.
|
||||
- **Меню** — кнопка `⋮` в шапке открывает выпадающее окошко с пунктом
|
||||
«О приложении» (описание и версия сборки).
|
||||
- **Меню** — кнопка `⋮` в шапке открывает выпадающее окошко с пунктами
|
||||
«Карта» и «О приложении». Меню сворачивается после выбора пункта, по нажатию
|
||||
вне его и по кнопке «‹» (которая в этом случае только закрывает меню,
|
||||
а не уходит назад).
|
||||
- Тёмная тема, живые часы МСК, подсветка ближайших рейсов (< 15 мин).
|
||||
|
||||
## Технические данные
|
||||
@@ -97,6 +105,7 @@ apk-webview/ # исходники APK (WebView)
|
||||
│ ├── java/ru/ua1zbe/bus127/
|
||||
│ │ ├── MainActivity.java # главный экран: WebView + запрос разрешения
|
||||
│ │ ├── AboutActivity.java # окно «О приложении»
|
||||
│ │ ├── MapActivity.java # экран карты (свой WebView на тот же index.html#map)
|
||||
│ │ ├── Webs.java # общий WebView для всех экранов
|
||||
│ │ ├── BusBridge.java # мост страницы и приложения (меню, назад, версия)
|
||||
│ │ ├── Bus127App.java # инициализация RuStore Push SDK
|
||||
@@ -104,6 +113,9 @@ apk-webview/ # исходники APK (WebView)
|
||||
│ │ ├── PushIntents.java # переход по клику
|
||||
│ │ └── push/PushStore.java # хранение push-токена
|
||||
│ ├── 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
|
||||
└── settings.gradle, build.gradle, gradle.properties
|
||||
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 Картами.
|
||||
|
||||
@@ -19,8 +19,8 @@ android {
|
||||
applicationId "ru.ua1zbe.bus127"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 12
|
||||
versionName "2.6.3"
|
||||
versionCode 13
|
||||
versionName "2.6.4"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -40,6 +40,13 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".MapActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/map_title"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:exported="false"
|
||||
|
||||
@@ -94,6 +94,43 @@
|
||||
.aboutcard p{margin:0 0 10px;font-size:14px;line-height:1.5}
|
||||
.aboutcard p.muted{color:var(--muted);font-size:13px}
|
||||
|
||||
/* экран «Карта»: страница целиком занята картой, шапка остаётся */
|
||||
body.map{padding:24px 16px 16px;overflow:hidden}
|
||||
body.map .wrap{height:100vh;height:100dvh;display:flex;flex-direction:column;max-width:none}
|
||||
body.map header{margin-bottom:12px}
|
||||
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}
|
||||
.mapstatus{flex:0 0 auto;background:var(--card);border:1px solid var(--line);
|
||||
border-radius:14px;padding:11px 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}
|
||||
|
||||
/* метки на карте. Корень двигает библиотека, вид задаёт внутренний блок */
|
||||
.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;
|
||||
overflow:hidden;display:none;margin-bottom:8px}
|
||||
/* переключатель типа дня в полном расписании */
|
||||
@@ -131,6 +168,7 @@
|
||||
</div>
|
||||
|
||||
<nav class="menu" id="menuBox" hidden>
|
||||
<button class="menuitem" id="mMap">Карта</button>
|
||||
<button class="menuitem" id="mAbout">О приложении</button>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -166,6 +204,16 @@
|
||||
<div id="tableBody"></div>
|
||||
</section>
|
||||
|
||||
<section class="mapcard" id="mapBox" hidden>
|
||||
<div class="mapcanvas loading" id="mapCanvas"></div>
|
||||
<div class="mapstatus">
|
||||
<div class="head" id="mapHead">Карта маршрута</div>
|
||||
<div class="rows" id="mapRows"></div>
|
||||
<div class="hint">Положение автобуса рассчитано по расписанию, а не по GPS:
|
||||
между остановками интерполируется по дороге. Точность — до нескольких минут.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="aboutcard" id="aboutBox">
|
||||
<h2>О приложении</h2>
|
||||
<div class="ver" id="aboutVer"></div>
|
||||
@@ -173,8 +221,12 @@
|
||||
<p>Ближайшие рейсы считаются по московскому времени и обновляются каждые 30 секунд.
|
||||
Полное расписание раскрывается прямо на этом экране — кнопкой под списком рейсов,
|
||||
с переключением между буднями, субботой и воскресеньем.</p>
|
||||
<p>В меню есть «Карта»: там видно, где сейчас едут автобусы, и как пройден
|
||||
участок между остановками. Положение считается по расписанию — приложение
|
||||
не получает данные с GPS, поэтому это оценка с точностью до нескольких минут.</p>
|
||||
<p class="muted">Расписание действует с 30.09.2020. Время московское (МСК).</p>
|
||||
<p class="muted">Пуш-уведомления приходят через RuStore Push — для этого нужен
|
||||
<p class="muted">Карта и тайлы — VK Карты, геометрия дороги — OpenStreetMap.
|
||||
Пуш-уведомления приходят через RuStore Push — для этого нужен
|
||||
установленный и авторизованный RuStore на устройстве.</p>
|
||||
</section>
|
||||
|
||||
@@ -410,12 +462,14 @@ document.getElementById("fullBtn").addEventListener("click",()=>{
|
||||
// Режим показа задаётся якорью в адресе:
|
||||
// (пусто) — браузер;
|
||||
// #app — главный экран приложения;
|
||||
// #map — карта с положением автобуса;
|
||||
// #about — окно «О приложении».
|
||||
// В приложении (не в браузере) страница дёргает нативный код через объект Bus.
|
||||
const MODE = location.hash.replace("#","") || "web";
|
||||
const NATIVE = MODE !== "web" && typeof Bus !== "undefined";
|
||||
|
||||
document.body.classList.add(MODE);
|
||||
document.getElementById("mapBox").hidden = MODE !== "map";
|
||||
if (MODE === "about" && NATIVE) {
|
||||
// версию берём из приложения, чтобы не расходилась со сборкой
|
||||
const v = document.getElementById("aboutVer");
|
||||
@@ -429,8 +483,13 @@ if (NATIVE) {
|
||||
e.stopPropagation();
|
||||
menuBox.hidden = !menuBox.hidden;
|
||||
});
|
||||
document.getElementById("navBack").addEventListener("click",()=>Bus.goBack());
|
||||
document.getElementById("mAbout").addEventListener("click",()=>Bus.openAbout());
|
||||
document.getElementById("navBack").addEventListener("click",()=>{
|
||||
// «назад» сначала сворачивает открытое меню и только потом уходит назад
|
||||
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=>{
|
||||
if (!e.target.closest(".menu") && !e.target.closest("#navMenu")) closeMenu();
|
||||
});
|
||||
@@ -442,6 +501,255 @@ tickClock(); renderDay(); render();
|
||||
setInterval(tickClock,1000);
|
||||
setInterval(render,30000);
|
||||
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;
|
||||
|
||||
// Длина ломаной с накопленными отрезками. Расстояние считаем в
|
||||
// равнопромежуточной проекции — на таком масштабе это точнее градусов.
|
||||
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];
|
||||
}
|
||||
|
||||
// Рейс разложен на четыре участка: туда Кандалакша → ПЗ → Зашеек и обратно.
|
||||
// Время в пути ПЗ → Кандалакша в таблице не указано, берём те же 50 минут,
|
||||
// что и в DIRECTIONS.
|
||||
function makeRun(row){
|
||||
const t0=toMin(row[0]), t1=toMin(row[1]), t2=toMin(row[2]), t3=toMin(row[3]);
|
||||
const back = t3 + RIDE_PZ_KAND; // прибытие в Кандалакшу
|
||||
return {
|
||||
t0, tEnd: back,
|
||||
segs: [
|
||||
{a:t0, b:t1, line:KAND_TO_PZ, to:"Полярные Зори", end:t1},
|
||||
{a:t1, b:t2, line:PZ_TO_ZASHEEK, to:"Зашеек", end:t2},
|
||||
{a:t2, b:t3, line:PZ_TO_ZASHEEK_BACK, to:"Полярные Зори", end:t3},
|
||||
{a:t3, b:back, line:KAND_TO_PZ_BACK, to:"Кандалакша", end:back}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Все рейсы, которые сейчас в пути, вместе с точкой и участком
|
||||
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.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 =>
|
||||
`<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
|
||||
});
|
||||
|
||||
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>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (MODE === "map") initMap();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
150
apk-webview/app/src/main/assets/mmr-gl.LICENSE.txt
Normal file
150
apk-webview/app/src/main/assets/mmr-gl.LICENSE.txt
Normal 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.
|
||||
1
apk-webview/app/src/main/assets/mmr-gl.css
Normal file
1
apk-webview/app/src/main/assets/mmr-gl.css
Normal file
File diff suppressed because one or more lines are too long
48
apk-webview/app/src/main/assets/mmr-gl.js
Normal file
48
apk-webview/app/src/main/assets/mmr-gl.js
Normal file
File diff suppressed because one or more lines are too long
7
apk-webview/app/src/main/assets/route.js
Normal file
7
apk-webview/app/src/main/assets/route.js
Normal 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]];
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
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_MAP = "file:///android_asset/index.html#map";
|
||||
/** Описание приложения и версия. */
|
||||
static final String URL_ABOUT = "file:///android_asset/index.html#about";
|
||||
|
||||
@@ -22,7 +27,25 @@ final class Webs {
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setSupportZoom(false);
|
||||
// Страница лежит в assets и грузится по file://, а карта (#map) тянет
|
||||
// стиль, тайлы и шрифты VK Карт с https. Без этого file://-страница
|
||||
// не может обращаться к внешним адресам, и карта остаётся пустой.
|
||||
settings.setAllowUniversalAccessFromFileURLs(true);
|
||||
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.loadUrl(url);
|
||||
return webView;
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
<string name="push_channel_id">bus127_push</string>
|
||||
<string name="push_channel_name">Пуш-уведомления</string>
|
||||
|
||||
<string name="map_title">Карта</string>
|
||||
<string name="about_title">О приложении</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user