├── fe
├── robots.txt
├── arrow.png
├── bus_round.png
├── favicon.ico
├── geolocation.png
├── bus_round_lf.png
├── img
│ ├── bus_round.png
│ ├── bus_round_lf.png
│ ├── bus_round_wait.png
│ ├── bus_round_lf_wait.png
│ ├── bus_round_long_wait.png
│ └── bus_round_lf_long_wait.png
├── auto-complete.css
├── businfo.html
├── map.html
├── map_only.html
├── index.html
├── feedback.html
├── map.css
├── promises.min.js
├── busstops.html
├── busroutes.html
├── auto-complete.min.js
├── fetch.min.js
├── busroutes.js
├── main.js
├── busstops.js
└── map.js
├── CNAME
├── Procfile
├── runtime.txt
├── .env.example
├── test_data.7z
├── libs
├── libfbclient.so
├── libfbclient.so.2
├── libtommath.so.1
├── libfbclient.so.3.0.5
├── libspatialindex.so.4
├── libspatialindex_c.so
├── libtommath.so.1.2.0
└── libspatialindex_c.so.4
├── tox.ini
├── .gitignore
├── docs
└── index.html
├── requirements.txt
├── models.py
├── settings.py.tmpl
├── README.md
├── save_test_data.py
├── alembic
└── versions
│ └── 97bd8c40e2d5_init.py
├── db.py
├── test_helpers.py
├── bus_routes_codd.json
├── fotobus_scrapper.py
├── alembic_dev.ini
├── index.html
├── abuse_checker.py
├── main.py
├── test_tracking.py
├── tracking.py
├── data_types.py
├── helpers.py
├── test_cds.py
├── data_processors.py
├── website.py
├── data_providers.py
└── tgbot.py
/fe/robots.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | test.vrntrans.ru
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: python main.py
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-3.8.12
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DUMP_DB_REQUESTS = True
--------------------------------------------------------------------------------
/fe/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/arrow.png
--------------------------------------------------------------------------------
/test_data.7z:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/test_data.7z
--------------------------------------------------------------------------------
/fe/bus_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/bus_round.png
--------------------------------------------------------------------------------
/fe/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/favicon.ico
--------------------------------------------------------------------------------
/fe/geolocation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/geolocation.png
--------------------------------------------------------------------------------
/fe/bus_round_lf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/bus_round_lf.png
--------------------------------------------------------------------------------
/fe/img/bus_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/img/bus_round.png
--------------------------------------------------------------------------------
/libs/libfbclient.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libfbclient.so
--------------------------------------------------------------------------------
/libs/libfbclient.so.2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libfbclient.so.2
--------------------------------------------------------------------------------
/libs/libtommath.so.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libtommath.so.1
--------------------------------------------------------------------------------
/fe/img/bus_round_lf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/img/bus_round_lf.png
--------------------------------------------------------------------------------
/fe/img/bus_round_wait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/img/bus_round_wait.png
--------------------------------------------------------------------------------
/libs/libfbclient.so.3.0.5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libfbclient.so.3.0.5
--------------------------------------------------------------------------------
/libs/libspatialindex.so.4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libspatialindex.so.4
--------------------------------------------------------------------------------
/libs/libspatialindex_c.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libspatialindex_c.so
--------------------------------------------------------------------------------
/libs/libtommath.so.1.2.0:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libtommath.so.1.2.0
--------------------------------------------------------------------------------
/fe/img/bus_round_lf_wait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/img/bus_round_lf_wait.png
--------------------------------------------------------------------------------
/libs/libspatialindex_c.so.4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/libs/libspatialindex_c.so.4
--------------------------------------------------------------------------------
/fe/img/bus_round_long_wait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/img/bus_round_long_wait.png
--------------------------------------------------------------------------------
/fe/img/bus_round_lf_long_wait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vrntrans/vrnbus/HEAD/fe/img/bus_round_lf_long_wait.png
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .git,
4 | __pycache__,
5 | fe,
6 | logs
7 | max-complexity = 8
8 | max-line-length = 110
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /logs/*
2 | /__pycache__/*
3 | /settings.py
4 | /test_data/*
5 | /.idea/*
6 | /.ssh/*
7 | *.dll
8 | *.dll
9 | /venv/*
10 | /.env
11 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | https://vrnbus.herokuapp.com
7 |
8 |
9 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | python-telegram-bot==11.1.0
2 | cachetools==2.0.1
3 | tornado==6.1
4 | pytz==2017.3
5 | fdb==2.0.1
6 | beautifulsoup4==4.8.0
7 | freezegun~=1.1.0
8 | APScheduler==3.5.3
9 | Rtree==0.8.3
10 | requests==2.22.0
11 | psycopg2==2.8.6
12 | psycopg2-binary==2.8.6
13 | sqlalchemy==1.3.23
14 | python-dotenv==0.15.0
15 | firebird-driver~=1.3.4
16 |
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, String
2 |
3 | from db import Base
4 | from sqlalchemy import Column, String
5 |
6 | from db import Base
7 |
8 |
9 | class RouteEdges(Base):
10 | __tablename__ = 'route_edges'
11 |
12 | edge_key = Column(String(100), nullable=False, unique=True, index=True, primary_key=True)
13 | points = Column(String, nullable=False, default='')
14 |
15 | def __str__(self):
16 | return f"User({self.first_name} {self.last_name} | {self.email})"
17 |
18 |
--------------------------------------------------------------------------------
/settings.py.tmpl:
--------------------------------------------------------------------------------
1 | CDS_HOST = '192.168.0.1'
2 | CDS_DB_PATH = r'C:\DB\PROJECTS.FDB'
3 | CDS_DB_PATH = r'C:\DB\PROJECTS.FDB'
4 | CDS_DB_DATA_PATH = r'C:\DB\DATA.FDB'
5 | CDS_DB_PROJECTS_PATH = r'C:\DB\PROJECTS.FDB'
6 | CDS_USER = 'SYSDBA'
7 | CDS_PASS = 'masterkey'
8 | VRNBUSBOT_TOKEN = ''
9 | PING_HOST = 'http://localhost:8080'
10 | USERS_TO_INFORM = ""
11 | VRNBUSBOT_TOKEN = ''
12 |
13 | LOAD_TEST_DATA = True
14 | FULL_ACCESS_KEY = ""
15 | DB_LINK = 'postgresql://vrnbus:vrnbus@127.0.0.1/vrnbus'
16 |
17 |
--------------------------------------------------------------------------------
/fe/auto-complete.css:
--------------------------------------------------------------------------------
1 | .autocomplete-suggestions {
2 | text-align: left; cursor: default; border: 1px solid #ccc; border-top: 0; background: #fff; box-shadow: -1px 1px 3px rgba(0,0,0,.1);
3 |
4 | /* core styles should not be changed */
5 | position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box;
6 | }
7 | .autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.02em; color: #333; }
8 | .autocomplete-suggestion b { font-weight: normal; color: #1f8dd6; }
9 | .autocomplete-suggestion.selected { background: #f0f0f0; }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vrnbus
2 | Прогноз прибытия автобусов в Воронеже.
3 | Веб-версия: https://vrnbus.herokuapp.com
4 | Телеграм-бот: https://t.me/vrnbusbot
5 |
6 | # Техническое
7 |
8 | Front-end: чистый JavaScript и поддержка fetch/promises для старых браузеров
9 |
10 | Back-end: Python 3.6 (cachetools, fdb, python-telegram-bot, pytz, tornado)
11 |
12 | # Видео о проекте
13 | https://www.youtube.com/watch?v=1OtHwGqSL04
14 |
15 | # Установка
16 | * Установить Python 3.6 или новее
17 | * `pip install -r requirements.txt`
18 | * Распаковать `test_data.7z` в каталог `test_data`
19 | * Создать Телеграм-бота для тестов с помощью бота
20 | [@BotFather](https://t.me/BotFather) и получить его токен
21 | * Указать токен в `settings.py` или в переменной окружения VRNBUSBOT_TOKEN
22 | * Запустить `main.py` и открыть [http://localhost:8080](http://localhost:8080).
--------------------------------------------------------------------------------
/save_test_data.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | import datetime
3 | import json
4 | import logging
5 | import time
6 | from pathlib import Path
7 |
8 | from data_providers import CdsDBDataProvider
9 | from helpers import CustomJsonEncoder
10 |
11 |
12 | def save_test_data(file_numbers=500, sleep_time=30):
13 | logging.basicConfig(format='%(asctime)s - %(levelname)s [%(filename)s:%(lineno)s %(funcName)20s] %(message)s',
14 | level=logging.INFO,
15 | handlers=[logging.StreamHandler()])
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 | if not Path('test_data').is_dir():
20 | Path('test_data').mkdir()
21 |
22 | cds = CdsDBDataProvider(logger)
23 | logger.info("Start")
24 | for i in range(file_numbers):
25 | codd_data_db = cds.load_all_cds_buses()
26 | now = datetime.datetime.now()
27 | with open(f'test_data/codd_data_db{now:%y_%m_%d_%H_%M_%S}.json', 'wb') as f:
28 | json.dump(codd_data_db, codecs.getwriter('utf-8')(f), ensure_ascii=False, indent=1, cls=CustomJsonEncoder)
29 | time.sleep(sleep_time)
30 |
31 | logger.info("Stop")
32 |
33 |
34 | if __name__ == '__main__':
35 | save_test_data()
36 |
--------------------------------------------------------------------------------
/alembic/versions/97bd8c40e2d5_init.py:
--------------------------------------------------------------------------------
1 | """Init
2 |
3 | Revision ID: 97bd8c40e2d5
4 | Revises:
5 | Create Date: 2019-10-07 23:39:56.524903
6 |
7 | """
8 | import sqlalchemy as sa
9 | from alembic import op
10 |
11 | # revision identifiers, used by Alembic.
12 | revision = '97bd8c40e2d5'
13 | down_revision = None
14 | branch_labels = None
15 | depends_on = None
16 |
17 |
18 | def upgrade():
19 | # ### commands auto generated by Alembic - please adjust! ###
20 | op.create_table('route_edges',
21 | sa.Column('created_at', sa.DateTime(), nullable=True),
22 | sa.Column('updated_at', sa.DateTime(), nullable=True),
23 | sa.Column('edge_key', sa.String(length=100), nullable=False),
24 | sa.Column('points', sa.String(), nullable=False),
25 | sa.PrimaryKeyConstraint('edge_key')
26 | )
27 | op.create_index(op.f('ix_route_edges_created_at'), 'route_edges', ['created_at'], unique=False)
28 | op.create_index(op.f('ix_route_edges_edge_key'), 'route_edges', ['edge_key'], unique=True)
29 | op.create_index(op.f('ix_route_edges_updated_at'), 'route_edges', ['updated_at'], unique=False)
30 | # ### end Alembic commands ###
31 |
32 |
33 | def downgrade():
34 | # ### commands auto generated by Alembic - please adjust! ###
35 | op.drop_index(op.f('ix_route_edges_updated_at'), table_name='route_edges')
36 | op.drop_index(op.f('ix_route_edges_edge_key'), table_name='route_edges')
37 | op.drop_index(op.f('ix_route_edges_created_at'), table_name='route_edges')
38 | op.drop_table('route_edges')
39 | # ### end Alembic commands ###
40 |
--------------------------------------------------------------------------------
/db.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from contextlib import contextmanager
4 | from datetime import datetime
5 |
6 | from sqlalchemy import create_engine, Column, DateTime
7 | # from sqlalchemy.ext.declarative import declarative_base
8 | from sqlalchemy.ext.declarative import as_declarative
9 | from sqlalchemy.orm import sessionmaker
10 |
11 | logger = logging.getLogger("db")
12 | try:
13 | import settings
14 |
15 | DB_LINK = settings.DB_LINK
16 | except ImportError:
17 | env = os.environ
18 | DB_LINK = env.get('DB_LINK')
19 |
20 | engine = create_engine(DB_LINK)
21 |
22 |
23 | def add_row(entity):
24 | session = Session()
25 | session.add(entity)
26 | session.commit()
27 |
28 |
29 | @as_declarative()
30 | class Base:
31 | created_at = Column(DateTime, default=datetime.utcnow, index=True)
32 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
33 |
34 |
35 | def __repr__(self):
36 | return str(self)
37 |
38 | def __str__(self):
39 | return f'{self.__class__.__name__} {self.created_at} {self.updated_at}'
40 |
41 | Session = sessionmaker(bind=engine)
42 |
43 |
44 | @contextmanager
45 | def session_scope(scope_info=None):
46 | """Provide a transactional scope around a series of operations."""
47 | session = Session()
48 | from sqlalchemy.orm.exc import NoResultFound
49 | try:
50 | yield session
51 | session.commit()
52 | except NoResultFound:
53 | logger.exception(scope_info)
54 | session.rollback()
55 | except Exception as ex:
56 | logger.exception(scope_info)
57 | session.rollback()
58 | raise
59 | finally:
60 | session.close()
--------------------------------------------------------------------------------
/test_helpers.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | import helpers
4 |
5 |
6 | class TestFuzzySearch(unittest.TestCase):
7 | def test_different_cases(self):
8 | f = helpers.fuzzy_search_advanced
9 | cases = [
10 | ('кирова в центр', 'ул. Кирова (в центр)', True),
11 | ('кирова', 'ул. Кирова (в центр)', True),
12 | ('кирова', 'ДК им. Кирова (ул. Героев Сибиряков из центра)', True),
13 | ('кирова в центр', 'ДК им. Кирова (ул. Героев Сибиряков из центра)', False),
14 | ('кирова в центр', 'ДК им. Кирова (ул. Героев Сибиряков из центра)', False),
15 | ('дк кир лен', 'ДК им. Кирова (Ленинский пр-т в сторону ул. Димитрова)', True),
16 | ('дк кир лен', 'ДК им. Кирова (Ленинский проспект в сторону Машмета)', True),
17 | ('брно', 'Мебель Черноземья (в центр)', False),
18 | ('автовокзал в', 'автовокзал (в центр)', True),
19 | ('автовокзал в', 'Центральный автовокзал (в центр)', True),
20 | ]
21 |
22 | for (needle, haystack, result) in cases:
23 | with self.subTest(f'{needle}, {haystack}, {result}'):
24 | self.assertEqual(f(needle, haystack), result)
25 |
26 |
27 | class TestGeoFunction(unittest.TestCase):
28 | def test_azimuth(self):
29 | f = helpers.azimuth
30 | cases = [
31 | ((24.323810, 1.368795, 39.169720, 51.652228), 12),
32 | ((241.323810, 1.468795, 39.182616, 51.697372), 16),
33 | ]
34 |
35 | for (params, result) in cases:
36 | with self.subTest(f'{params}, {result}'):
37 | self.assertEqual(f(*params), result)
38 |
39 |
40 | if __name__ == '__main__':
41 | unittest.main()
42 |
--------------------------------------------------------------------------------
/fe/businfo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | vrnbus (beta) | Поиск по автобусам Воронежа
9 |
10 |
11 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/bus_routes_codd.json:
--------------------------------------------------------------------------------
1 | {
2 | "104": 53,
3 | "105": 63,
4 | "108А": 73,
5 | "10А": 67,
6 | "11": 68,
7 | "113": 46,
8 | "113КТ": 121,
9 | "113КШ": 131,
10 | "115": 115,
11 | "120": 15,
12 | "120В": 39,
13 | "122": 13,
14 | "125": 62,
15 | "125А": 111,
16 | "13": 77,
17 | "14В": 136,
18 | "15": 92,
19 | "15А": 78,
20 | "16В": 123,
21 | "17": 55,
22 | "18": 74,
23 | "1КВ": 30,
24 | "1КС": 29,
25 | "20": 82,
26 | "20Б": 113,
27 | "20М": 88,
28 | "22": 101,
29 | "23К": 18,
30 | "25А": 52,
31 | "26А": 69,
32 | "27": 9,
33 | "3": 49,
34 | "33К": 19,
35 | "34": 80,
36 | "37": 81,
37 | "37А": 85,
38 | "39": 59,
39 | "3В": 75,
40 | "41": 34,
41 | "42": 187,
42 | "43": 163,
43 | "47": 45,
44 | "48": 133,
45 | "49": 22,
46 | "49А": 104,
47 | "49Б": 24,
48 | "49М": 144,
49 | "5": 48,
50 | "50": 126,
51 | "52": 1,
52 | "54": 66,
53 | "57В": 12,
54 | "58В": 16,
55 | "59": 134,
56 | "59А": 91,
57 | "59АС": 106,
58 | "5А": 8,
59 | "6": 100,
60 | "60Б": 35,
61 | "61": 129,
62 | "62": 61,
63 | "64": 57,
64 | "64А": 200,
65 | "65": 76,
66 | "65А": 97,
67 | "66": 58,
68 | "67А": 33,
69 | "68": 54,
70 | "68А": 198,
71 | "68Т": 118,
72 | "69Т": 4,
73 | "6М": 60,
74 | "70А": 50,
75 | "70М": 21,
76 | "75": 72,
77 | "76": 31,
78 | "77К": 44,
79 | "78А": 108,
80 | "79": 65,
81 | "8": 32,
82 | "80": 2,
83 | "81": 70,
84 | "84": 56,
85 | "87": 145,
86 | "88": 23,
87 | "88А": 83,
88 | "90": 64,
89 | "91": 109,
90 | "93": 130,
91 | "98": 140,
92 | "9КА": 38,
93 | "9КС": 51,
94 | "Тр.11": 41,
95 | "Тр.17": 40,
96 | "Тр.7": 6,
97 | "Тр.8": 42
98 | }
--------------------------------------------------------------------------------
/fotobus_scrapper.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | from typing import List
4 |
5 | import requests
6 | from bs4 import BeautifulSoup
7 |
8 | fb_name_re = re.compile(r"(\D+)(\d+)(\D+)(\d+)")
9 | fb_name_re_2 = re.compile(r"(\D+)(\d+)(\d{2})")
10 |
11 | def get_name_with_spaces(bus_name):
12 | match = fb_name_re.match(bus_name)
13 | if not match:
14 | match = fb_name_re_2.match(bus_name)
15 | if not match:
16 | return
17 |
18 | return " ".join(match.groups())
19 |
20 | def get_url(bus_name):
21 | name_w_spaces = get_name_with_spaces(bus_name)
22 | if not name_w_spaces:
23 | return
24 | return "http://fotobus.msk.ru/ajax2.php?action=index-qsearch&cid=0&type=1&num=" + name_w_spaces
25 |
26 | def get_bus_search_page(bus_name):
27 | url = get_url(bus_name)
28 | if not url:
29 | return
30 |
31 | result = requests.get(url, allow_redirects=True)
32 | print(result.url)
33 | return result.content
34 |
35 | def get_fb_links(content):
36 | soup = BeautifulSoup(content, features="html.parser")
37 | anchors = soup.find_all("a")
38 | hrefs = {a.get('href').split('#')[0] for a in anchors}
39 | print(hrefs)
40 | return [f"http://fotobus.msk.ru{h}" for h in hrefs if not h.startswith('http')]
41 |
42 |
43 | def fb_links(bus_name):
44 | url = get_url(bus_name)
45 | if not url:
46 | return []
47 | content = get_bus_search_page(bus_name)
48 | links = get_fb_links(content)
49 | return links or [url, ]
50 |
51 |
52 |
53 | if __name__ == '__main__':
54 | start = time.time()
55 | content = get_bus_search_page("Е312УС36")
56 | result = get_fb_links(content)
57 | print(result)
58 | print(time.time() - start)
59 | content = get_bus_search_page("ВВ37336")
60 | result = get_fb_links(content)
61 | print(result)
62 | print(time.time() - start)
63 | print(fb_links("ВВ37336"))
--------------------------------------------------------------------------------
/fe/map.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vrnbus (beta) | Карта автобусов Воронежа
7 |
8 |
9 |
10 |
11 |
12 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
41 |
42 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/alembic_dev.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # timezone to use when rendering the date
11 | # within the migration file as well as the filename.
12 | # string value is passed to dateutil.tz.gettz()
13 | # leave blank for localtime
14 | # timezone =
15 |
16 | # max length of characters to apply to the
17 | # "slug" field
18 | #truncate_slug_length = 40
19 |
20 | # set to 'true' to run the environment during
21 | # the 'revision' command, regardless of autogenerate
22 | # revision_environment = false
23 |
24 | # set to 'true' to allow .pyc and .pyo files without
25 | # a source .py file to be detected as revisions in the
26 | # versions/ directory
27 | # sourceless = false
28 |
29 | # version location specification; this defaults
30 | # to alembic/versions. When using multiple version
31 | # directories, initial revisions must be specified with --version-path
32 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions
33 |
34 | # the output encoding used when revision files
35 | # are written from script.py.mako
36 | # output_encoding = utf-8
37 |
38 | # sqlalchemy.url = driver://user:pass@localhost/dbname
39 | sqlalchemy.url = postgresql://vrnbus:vrnbus@127.0.0.1/vrnbus
40 |
41 |
42 | # Logging configuration
43 | [loggers]
44 | keys = root,sqlalchemy,alembic
45 |
46 | [handlers]
47 | keys = console
48 |
49 | [formatters]
50 | keys = generic
51 |
52 | [logger_root]
53 | level = WARN
54 | handlers = console
55 | qualname =
56 |
57 | [logger_sqlalchemy]
58 | level = WARN
59 | handlers =
60 | qualname = sqlalchemy.engine
61 |
62 | [logger_alembic]
63 | level = INFO
64 | handlers =
65 | qualname = alembic
66 |
67 | [handler_console]
68 | class = StreamHandler
69 | args = (sys.stderr,)
70 | level = NOTSET
71 | formatter = generic
72 |
73 | [formatter_generic]
74 | format = %(levelname)-5.5s [%(name)s] %(message)s
75 | datefmt = %H:%M:%S
76 |
--------------------------------------------------------------------------------
/fe/map_only.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vrnbus (beta) | Карта автобусов Воронежа
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
41 |
42 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
37 |
38 | https://vrnbus.herokuapp.com
39 |
40 |
41 |
--------------------------------------------------------------------------------
/abuse_checker.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 | from collections import defaultdict, deque
4 | from typing import List
5 |
6 | from data_types import AbuseRule
7 |
8 | BLACK_LIST = os.environ.get('BLACK_LIST', '')
9 |
10 | def last_time(delta: datetime.timedelta):
11 | return datetime.datetime.now() - delta
12 |
13 |
14 | class AbuseChecker:
15 | def __init__(self, logger, rules: List[AbuseRule]):
16 | self.logger = logger
17 | self.events = {}
18 | self.default_rule = AbuseRule(0, 100, datetime.timedelta(minutes=60))
19 | self.rules = {v.event: v for v in rules}
20 | for rule in rules:
21 | self.events[rule.event] = defaultdict(lambda: deque(maxlen=rule.count))
22 |
23 | def reset_stats(self, user_id):
24 | self.events[user_id].clear()
25 |
26 | def prepare_dict(self, event):
27 | if event in self.events:
28 | return
29 | rule = self.rules.get(event)
30 | if rule:
31 | self.logger.error(f"There is no prepared dict for {event}")
32 | self.events[event] = defaultdict(lambda: deque(maxlen=rule.count))
33 | return
34 |
35 | self.logger.error(f"There is no rule for {event}")
36 | self.events[event] = defaultdict(lambda: deque(maxlen=self.default_rule.count))
37 |
38 | def check_time(self):
39 | now = datetime.datetime.now()
40 | return now.hour >= 20 or now.hour <= 7
41 |
42 | def check_user(self, event, user_id):
43 | if user_id in BLACK_LIST:
44 | return False
45 |
46 | if self.check_time():
47 | return True
48 |
49 | self.prepare_dict(event)
50 | user_events = self.events[event][user_id]
51 | rule = self.rules.get(event, self.default_rule)
52 | if len(user_events) < rule.count:
53 | return True
54 |
55 | min_time = min(user_events)
56 | if min_time < last_time(rule.delta):
57 | return True
58 |
59 | return False
60 |
61 | def add_user_event(self, event, user_id):
62 | self.prepare_dict(event)
63 | self.events[event][user_id].append(datetime.datetime.now())
64 | return self.check_user(event, user_id)
65 |
--------------------------------------------------------------------------------
/fe/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | vrnbus (beta) | Прибытие на остановки
10 |
11 |
12 |
18 |
19 |
20 |
21 |
(Нужно разрешить сайту запрашивать геолокацию)
22 |
23 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | # #!/usr/bin/env python3.6
2 | import datetime
3 | import logging
4 | import os
5 | from logging.handlers import TimedRotatingFileHandler
6 | from pathlib import Path
7 |
8 | import tornado.web
9 | from dotenv import load_dotenv
10 | env_path = Path('.') / '.env'
11 | load_dotenv(dotenv_path=env_path)
12 |
13 | from abuse_checker import AbuseChecker
14 | from cds import CdsRequest
15 | from data_processors import WebDataProcessor
16 | from data_providers import get_data_provider
17 | from data_types import AbuseRule
18 | from tgbot import BusBot
19 | from tracking import EventTracker, WebEvent
20 | from website import BusSite
21 |
22 | if not Path('logs').is_dir():
23 | Path('logs').mkdir()
24 |
25 |
26 |
27 | # Enable logging
28 | file_handler = TimedRotatingFileHandler("logs/vrnbus.log", 'midnight', 1)
29 | file_handler.suffix = "%Y-%m-%d"
30 | logging.basicConfig(format='%(asctime)s.%(msecs)03d - %(levelname)s [%(filename)s:%(lineno)s %(funcName)10s] %(message)s',
31 | datefmt="%H:%M:%S",
32 | level=logging.INFO,
33 | handlers=[logging.StreamHandler(), file_handler])
34 |
35 | logger = logging.getLogger("vrnbus")
36 |
37 | logger.info([{k: os.environ[k]} for (k) in os.environ if 'PATH' not in k])
38 |
39 | user_settings = {}
40 |
41 | if __name__ == "__main__":
42 | log_ignore_events = [
43 | WebEvent.ABUSE,
44 | # WebEvent.FRAUD,
45 | # WebEvent.FULLINFO,
46 | WebEvent.IPCHANGE,
47 | WebEvent.BUSMAP,
48 | # WebEvent.ARRIVAL,
49 | WebEvent.ANDROID,
50 | WebEvent.IOS
51 | ]
52 |
53 | tracker = EventTracker(logger, log_ignore_events)
54 |
55 | abuse_rules = [
56 | AbuseRule(WebEvent.BUSMAP, 100, datetime.timedelta(minutes=30)),
57 | AbuseRule(WebEvent.BUSINFO, 100, datetime.timedelta(minutes=30)),
58 | ]
59 |
60 | anti_abuser = AbuseChecker(logger, abuse_rules)
61 | data_provider = get_data_provider(logger)
62 | cds = CdsRequest(logger, data_provider)
63 | data_processor = WebDataProcessor(cds, logger, tracker)
64 | bot = BusBot(cds, user_settings, logger, tracker)
65 | cds.wd_call_back = bot.broadcast_message
66 | application = BusSite(data_processor, logger, tracker, anti_abuser)
67 | application.listen(os.environ.get('PORT', 8088))
68 | tornado.ioloop.IOLoop.current().start()
69 |
--------------------------------------------------------------------------------
/fe/feedback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | vrnbus (beta) | О проекте
9 |
10 |
11 |
17 |
48 |
49 |
--------------------------------------------------------------------------------
/fe/map.css:
--------------------------------------------------------------------------------
1 | .bus-icon {
2 | position: absolute;
3 | width: 32px;
4 | height: 32px;
5 | margin-left: -5px;
6 | margin-top: 0;
7 | }
8 |
9 | .bus-title {
10 | display: block;
11 | background-color: white;
12 | position: absolute;
13 | margin-left: 25px;
14 | margin-top: 25px;
15 | padding: 2px;
16 | color: black;
17 | font-weight: bold;
18 | }
19 |
20 | .bus-title-top {
21 | margin-left: -10px;
22 | margin-top: -10px;
23 | }
24 |
25 | .bus-title-bottom {
26 | margin-left: 25px;
27 | margin-top: 25px;
28 | }
29 |
30 | .hide_info {
31 | display: none;
32 | }
33 |
34 | .spinner {
35 | display: inline-grid;
36 | height: 10px;
37 | width: 10px;
38 | margin: 0 auto;
39 | -webkit-animation: rotation .6s infinite linear;
40 | -moz-animation: rotation .6s infinite linear;
41 | -o-animation: rotation .6s infinite linear;
42 | animation: rotation .6s infinite linear;
43 | border: 2px solid rgba(0, 174, 239, .15);
44 | border-top-color: rgba(0, 174, 239, .8);
45 | border-radius: 100%;
46 | }
47 |
48 | @-webkit-keyframes rotation {
49 | from {
50 | -webkit-transform: rotate(0deg);
51 | }
52 | to {
53 | -webkit-transform: rotate(359deg);
54 | }
55 | }
56 |
57 | @-moz-keyframes rotation {
58 | from {
59 | -moz-transform: rotate(0deg);
60 | }
61 | to {
62 | -moz-transform: rotate(359deg);
63 | }
64 | }
65 |
66 | @-o-keyframes rotation {
67 | from {
68 | -o-transform: rotate(0deg);
69 | }
70 | to {
71 | -o-transform: rotate(359deg);
72 | }
73 | }
74 |
75 | @keyframes rotation {
76 | from {
77 | transform: rotate(0deg);
78 | }
79 | to {
80 | transform: rotate(359deg);
81 | }
82 | }
83 |
84 | div.input-row {
85 | width: 100%;
86 | overflow: hidden;
87 | max-width: 500px;
88 | padding: 3px;
89 | }
90 |
91 | input.input-row {
92 | width: 100%;
93 | }
94 |
95 | span.input-row {
96 | display: block;
97 | overflow: hidden;
98 | padding-right: 10px;
99 | }
100 |
101 | select.input-row {
102 | display: block;
103 | overflow: hidden;
104 | margin-right: 5px;
105 | }
106 |
107 | .input-row-right {
108 | float: right;
109 | }
110 |
111 | .input-row-left {
112 | float: left;
113 | }
114 |
115 | div.menu {
116 | padding-bottom: 10px;
117 | }
118 |
119 | div.menu > div {
120 | margin-right: 2%;
121 | display: inline;
122 | }
123 |
124 | div.menu > div:last-child {
125 | margin-right: 0;
126 | }
127 |
128 | div.map {
129 | width: 100%;
130 | height: 80vh;
131 | }
--------------------------------------------------------------------------------
/fe/promises.min.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n():"function"==typeof define&&define.amd?define(n):n()}(0,function(){"use strict";function e(){}function n(e){if(!(this instanceof n))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],f(e,this)}function t(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,n._immediateFn(function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var i;try{i=n(e._value)}catch(f){return void r(t.promise,f)}o(t.promise,i)}else(1===e._state?o:r)(t.promise,e._value)})):e._deferreds.push(t)}function o(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var o=t.then;if(t instanceof n)return e._state=3,e._value=t,void i(e);if("function"==typeof o)return void f(function(e,n){return function(){e.apply(n,arguments)}}(o,t),e)}e._state=1,e._value=t,i(e)}catch(u){r(e,u)}}function r(e,n){e._state=2,e._value=n,i(e)}function i(e){2===e._state&&0===e._deferreds.length&&n._immediateFn(function(){e._handled||n._unhandledRejectionFn(e._value)});for(var o=0,r=e._deferreds.length;r>o;o++)t(e,e._deferreds[o]);e._deferreds=null}function f(e,n){var t=!1;try{e(function(e){t||(t=!0,o(n,e))},function(e){t||(t=!0,r(n,e))})}catch(i){if(t)return;t=!0,r(n,i)}}var u=setTimeout;n.prototype["catch"]=function(e){return this.then(null,e)},n.prototype.then=function(n,o){var r=new this.constructor(e);return t(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(n,o,r)),r},n.prototype["finally"]=function(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})},n.all=function(e){return new n(function(n,t){function o(e,f){try{if(f&&("object"==typeof f||"function"==typeof f)){var u=f.then;if("function"==typeof u)return void u.call(f,function(n){o(e,n)},t)}r[e]=f,0==--i&&n(r)}catch(c){t(c)}}if(!e||"undefined"==typeof e.length)throw new TypeError("Promise.all accepts an array");var r=Array.prototype.slice.call(e);if(0===r.length)return n([]);for(var i=r.length,f=0;r.length>f;f++)o(f,r[f])})},n.resolve=function(e){return e&&"object"==typeof e&&e.constructor===n?e:new n(function(n){n(e)})},n.reject=function(e){return new n(function(n,t){t(e)})},n.race=function(e){return new n(function(n,t){for(var o=0,r=e.length;r>o;o++)e[o].then(n,t)})},n._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){u(e,0)},n._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var c=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==c)return c;throw Error("unable to locate global object")}();c.Promise||(c.Promise=n)});
2 |
--------------------------------------------------------------------------------
/fe/busstops.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
14 | vrnbus (beta) | Карта с остановками
15 |
16 |
17 |
23 |
24 |
25 |
26 |
(Нужно разрешить сайту запрашивать геолокацию)
27 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
51 |
52 |
56 |
57 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/fe/busroutes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
15 | vrnbus (beta) | Карта с маршрутами
16 |
17 |
18 |
24 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/test_tracking.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import logging
3 | import unittest
4 |
5 | from freezegun import freeze_time
6 |
7 | from abuse_checker import AbuseChecker
8 | from data_types import AbuseRule
9 | from tracking import EventTracker, TgEvent, WebEvent, get_event_by_name
10 |
11 | logging.basicConfig(format='%(asctime)s - %(levelname)s [%(filename)s:%(lineno)s %(funcName)20s] %(message)s',
12 | level=logging.INFO,
13 | handlers=[logging.StreamHandler()])
14 |
15 | logger = logging.getLogger("vrnbus")
16 |
17 |
18 | class FakeUser():
19 | def __init__(self, id=None):
20 | self.id = id if id else 42
21 |
22 |
23 | class TrackingEventTest(unittest.TestCase):
24 | def test_event_parsing(self):
25 | f = get_event_by_name
26 | cases = [
27 | (None, None),
28 | (123, None),
29 | ([], None),
30 | ([1, 2, 3], None),
31 | ('', None),
32 | ('Unknown_event', None),
33 | ('ARRIVAL', WebEvent.ARRIVAL),
34 | ('arrival', WebEvent.ARRIVAL),
35 | ('LaSt', TgEvent.LAST),
36 | ('Tg.last', TgEvent.LAST),
37 | ('Tg.abuse', TgEvent.ABUSE),
38 | ('web.abuse', WebEvent.ABUSE),
39 | ]
40 |
41 | for (event_name, result) in cases:
42 | with self.subTest(f'{event_name}, {result}'):
43 | self.assertEqual(f(event_name), result)
44 |
45 |
46 | class TrackingTest(unittest.TestCase):
47 | def test_something(self):
48 | tracker = EventTracker(logger)
49 | user = FakeUser()
50 |
51 | tracker.tg(TgEvent.START, user)
52 | tracker.web(WebEvent.ARRIVAL, '127.0.0.1')
53 | stats = tracker.stats()
54 | detailed_stats = tracker.stats(True)
55 | logger.info(stats)
56 | logger.info(detailed_stats)
57 |
58 | self.assertEqual(tracker.events[TgEvent.START], 1)
59 | self.assertEqual(tracker.events[WebEvent.ARRIVAL], 1)
60 | self.assertEqual(len(tracker.web_users), 1)
61 | self.assertEqual(len(tracker.tg_users), 1)
62 |
63 | def test_detailed_stats(self):
64 | logger.setLevel(logging.WARNING)
65 | tracker = EventTracker(logger)
66 | user = FakeUser()
67 |
68 | tracker.tg(TgEvent.START, user)
69 | for i in range(500):
70 | tracker.tg(TgEvent(i % 8 + 1), FakeUser(100500 + i % 7))
71 | tracker.web(WebEvent(i % 3 + 1), f'127.0.0.{i%3}')
72 | stats = tracker.stats()
73 | detailed_stats = tracker.stats(True)
74 |
75 | logger.setLevel(logging.INFO)
76 | logger.info(detailed_stats)
77 | self.assertNotEqual(stats, detailed_stats)
78 |
79 |
80 | class AbuseCheckerTest(unittest.TestCase):
81 | def test_wo_rules(self):
82 | checker = AbuseChecker(logger, [])
83 | checker.add_user_event(WebEvent.BUSMAP, '127.0.0.1')
84 |
85 | @freeze_time("12:00", tick=True)
86 | def test_with_rules_day(self):
87 | self.run_rules_check(False)
88 |
89 | @freeze_time("20:00", tick=True)
90 | def test_with_rules_evening(self):
91 | self.run_rules_check(True)
92 |
93 | @freeze_time("7:59", tick=True)
94 | def test_with_rules_morning(self):
95 | self.run_rules_check(True)
96 |
97 | def run_rules_check(self, check_result):
98 | abuse_rules = [
99 | AbuseRule(WebEvent.BUSINFO, 10, datetime.timedelta(minutes=60)),
100 | AbuseRule(WebEvent.BUSMAP, 10, datetime.timedelta(minutes=90)),
101 | ]
102 | checker = AbuseChecker(logger, abuse_rules)
103 | self.assertTrue(len(checker.rules) == 2)
104 | user_id = '127.0.0.1'
105 | for _ in range(50):
106 | checker.add_user_event(WebEvent.BUSMAP, user_id)
107 | checker.add_user_event(WebEvent.BUSINFO, user_id)
108 |
109 | self.assertEqual(checker.check_user(WebEvent.BUSMAP, user_id), check_result)
110 | self.assertEqual(checker.check_user(WebEvent.BUSINFO, user_id), check_result)
111 |
--------------------------------------------------------------------------------
/tracking.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from collections import defaultdict
3 | from enum import Enum, auto
4 | from typing import List
5 |
6 |
7 | class TgEvent(Enum):
8 | ABUSE = auto()
9 | START = auto()
10 | HELP = auto()
11 | LAST = auto()
12 | NEXT = auto()
13 | STATS = auto()
14 | USER_STATS = auto()
15 | LOCATION = auto()
16 | USER_INPUT = auto()
17 | CUSTOM_CMD = auto()
18 | WRONG_CMD = auto()
19 |
20 | @staticmethod
21 | def from_str(label):
22 | return TgEvent.__dict__.get(label)
23 |
24 |
25 | class WebEvent(Enum):
26 | ABUSE = auto()
27 | FRAUD = auto()
28 | ARRIVAL = auto()
29 | BUSINFO = auto()
30 | BUSMAP = auto()
31 | BUSSTOP = auto()
32 | FOTOBUS = auto()
33 | FULLINFO = auto()
34 | IPCHANGE = auto()
35 | IOS = auto()
36 | ANDROID = auto()
37 | WEB_SITE = auto()
38 | USER_STATS = auto()
39 | COMPLAIN = auto()
40 |
41 | @staticmethod
42 | def from_str(label):
43 | return WebEvent.__dict__.get(label)
44 |
45 |
46 | def get_event_by_name(name: str):
47 | if not name or not isinstance(name, str):
48 | return
49 | if '.' in name:
50 | parts = name.split('.')
51 | if len(parts) > 2:
52 | return
53 | (event_type, event_name) = (i.upper() for i in parts)
54 | if event_type == 'TG':
55 | return TgEvent.from_str(event_name)
56 | if event_type == 'WEB':
57 | return WebEvent.from_str(event_name)
58 |
59 | event_name = name.upper()
60 | web_event = WebEvent.from_str(event_name)
61 | if web_event:
62 | return web_event
63 | tg_event = TgEvent.from_str(event_name)
64 | if tg_event:
65 | return tg_event
66 |
67 |
68 | def get_events_by_names(str_events: List[str]):
69 | result = []
70 | for item in str_events:
71 | event_name = item.upper()
72 | web_event = WebEvent.get(event_name)
73 | if web_event:
74 | result.append(web_event)
75 | tg_event = TgEvent.get(event_name)
76 | if tg_event:
77 | result.append(tg_event)
78 |
79 |
80 | class EventTracker:
81 | def __init__(self, logger, log_ignore_events: List[Enum] = None):
82 | self.logger = logger
83 | self.log_ignore_events = log_ignore_events or []
84 | self.events = defaultdict(int)
85 | self.detailed_events = defaultdict(lambda: defaultdict(int))
86 | self.start = datetime.datetime.now()
87 | self.tg_users = set()
88 | self.web_users = set()
89 |
90 | def add_event(self, event, uid):
91 | self.events[event] += 1
92 | self.detailed_events[event][uid] += 1
93 |
94 | def reset(self):
95 | self.events = defaultdict(int)
96 | self.start = datetime.datetime.now()
97 | self.tg_users = set()
98 | self.web_users = set()
99 |
100 | def stats(self, detailed=False, details_treshold=50, user_filter='', event_filter=None):
101 | def replace_event_name(event):
102 | return str(event).replace("Event.", ".")
103 |
104 | events = [f'{replace_event_name(event_name):13} {sum(event_dict.values())} / {len(event_dict.keys())}'
105 | for event_name, event_dict in self.detailed_events.items()]
106 | events.sort()
107 | user_stats = "\n".join(events)
108 | tg_count = len(self.tg_users)
109 | web_count = len(self.web_users)
110 | user_types = f"Tg users: {tg_count}\nWeb users: {web_count}"
111 | full_info = ''
112 | if detailed:
113 | info_list = [f"{replace_event_name(event_name)}: {k} {v}"
114 | for event_name, event_dict in self.detailed_events.items()
115 | for k, v in event_dict.items()
116 | if v >= details_treshold
117 | or (user_filter and user_filter in k)
118 | or (event_filter and event_name in event_filter)
119 | ]
120 | full_info = "\nDetails\n" + "\n".join(sorted(info_list))
121 |
122 | return f'{self.start:%Y.%m.%d %H:%M}\n{user_stats}\n{user_types} {full_info}'
123 |
124 | def tg(self, event: TgEvent, user, *params):
125 | user_info = f"user:{user.id}"
126 | self.add_event(event, str(user.id))
127 | self.tg_users.add(user.id)
128 | if event not in self.log_ignore_events:
129 | self.logger.info(f"TRACK: {event} {user_info} {params if params else ''}")
130 |
131 | def web(self, event: WebEvent, ip, *params):
132 | self.add_event(event, ip)
133 | self.web_users.add(ip)
134 | if event not in self.log_ignore_events:
135 | self.logger.info(f"TRACK: {event} ip:{ip} {params if params else ''}")
136 |
--------------------------------------------------------------------------------
/fe/auto-complete.min.js:
--------------------------------------------------------------------------------
1 | // JavaScript autoComplete v1.0.4
2 | // https://github.com/Pixabay/JavaScript-autoComplete
3 | var autoComplete=function(){function e(e){function t(e,t){return e.classList?e.classList.contains(t):new RegExp("\\b"+t+"\\b").test(e.className)}function o(e,t,o){e.attachEvent?e.attachEvent("on"+t,o):e.addEventListener(t,o)}function s(e,t,o){e.detachEvent?e.detachEvent("on"+t,o):e.removeEventListener(t,o)}function n(e,s,n,l){o(l||document,s,function(o){for(var s,l=o.target||o.srcElement;l&&!(s=t(l,e));)l=l.parentElement;s&&n.call(l,o)})}if(document.querySelector){var l={selector:0,source:0,minChars:3,delay:150,offsetLeft:0,offsetTop:1,cache:1,menuClass:"",renderItem:function(e,t){t=t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&");var o=new RegExp("("+t.split(" ").join("|")+")","gi");return''+e.replace(o,"$1")+"
"},onSelect:function(){}};for(var c in e)e.hasOwnProperty(c)&&(l[c]=e[c]);for(var a="object"==typeof l.selector?[l.selector]:document.querySelectorAll(l.selector),u=0;u0?i.sc.scrollTop=n+i.sc.suggestionHeight+s-i.sc.maxHeight:0>n&&(i.sc.scrollTop=n+s)}else i.sc.scrollTop=0},o(window,"resize",i.updateSC),document.body.appendChild(i.sc),n("autocomplete-suggestion","mouseleave",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&setTimeout(function(){e.className=e.className.replace("selected","")},20)},i.sc),n("autocomplete-suggestion","mouseover",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&(e.className=e.className.replace("selected","")),this.className+=" selected"},i.sc),n("autocomplete-suggestion","mousedown",function(e){if(t(this,"autocomplete-suggestion")){var o=this.getAttribute("data-val");i.value=o,l.onSelect(e,o,this),i.sc.style.display="none"}},i.sc),i.blurHandler=function(){try{var e=document.querySelector(".autocomplete-suggestions:hover")}catch(t){var e=0}e?i!==document.activeElement&&setTimeout(function(){i.focus()},20):(i.last_val=i.value,i.sc.style.display="none",setTimeout(function(){i.sc.style.display="none"},350))},o(i,"blur",i.blurHandler);var r=function(e){var t=i.value;if(i.cache[t]=e,e.length&&t.length>=l.minChars){for(var o="",s=0;st||t>40)&&13!=t&&27!=t){var o=i.value;if(o.length>=l.minChars){if(o!=i.last_val){if(i.last_val=o,clearTimeout(i.timer),l.cache){if(o in i.cache)return void r(i.cache[o]);for(var s=1;s-1?e:t}function l(t,e){e=e||{};var r=e.body;if(t instanceof l){if(t.bodyUsed)throw new TypeError("Already read");this.url=t.url,this.credentials=t.credentials,e.headers||(this.headers=new n(t.headers)),this.method=t.method,this.mode=t.mode,r||null==t._bodyInit||(r=t._bodyInit,t.bodyUsed=!0)}else this.url=String(t);if(this.credentials=e.credentials||this.credentials||"omit",!e.headers&&this.headers||(this.headers=new n(e.headers)),this.method=y(e.method||this.method||"GET"),this.mode=e.mode||this.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&r)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(r)}function p(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function c(t){var e=new n;return t.split(/\r?\n/).forEach(function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}}),e}function b(t,e){e||(e={}),this.type="default",this.status="status"in e?e.status:200,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in e?e.statusText:"OK",this.headers=new n(e.headers),this.url=e.url||"",this._initBody(t)}if(!t.fetch){var m={searchParams:"URLSearchParams"in t,iterable:"Symbol"in t&&"iterator"in Symbol,blob:"FileReader"in t&&"Blob"in t&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in t,arrayBuffer:"ArrayBuffer"in t};if(m.arrayBuffer)var w=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],v=function(t){return t&&DataView.prototype.isPrototypeOf(t)},B=ArrayBuffer.isView||function(t){return t&&w.indexOf(Object.prototype.toString.call(t))>-1};n.prototype.append=function(t,o){t=e(t),o=r(o);var n=this.map[t];this.map[t]=n?n+","+o:o},n.prototype.delete=function(t){delete this.map[e(t)]},n.prototype.get=function(t){return t=e(t),this.has(t)?this.map[t]:null},n.prototype.has=function(t){return this.map.hasOwnProperty(e(t))},n.prototype.set=function(t,o){this.map[e(t)]=r(o)},n.prototype.forEach=function(t,e){for(var r in this.map)this.map.hasOwnProperty(r)&&t.call(e,this.map[r],r,this)},n.prototype.keys=function(){var t=[];return this.forEach(function(e,r){t.push(r)}),o(t)},n.prototype.values=function(){var t=[];return this.forEach(function(e){t.push(e)}),o(t)},n.prototype.entries=function(){var t=[];return this.forEach(function(e,r){t.push([r,e])}),o(t)},m.iterable&&(n.prototype[Symbol.iterator]=n.prototype.entries);var _=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];l.prototype.clone=function(){return new l(this,{body:this._bodyInit})},d.call(l.prototype),d.call(b.prototype),b.prototype.clone=function(){return new b(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new n(this.headers),url:this.url})},b.error=function(){var t=new b(null,{status:0,statusText:""});return t.type="error",t};var A=[301,302,303,307,308];b.redirect=function(t,e){if(A.indexOf(e)===-1)throw new RangeError("Invalid status code");return new b(null,{status:e,headers:{location:t}})},t.Headers=n,t.Request=l,t.Response=b,t.fetch=function(t,e){return new Promise(function(r,o){var n=new l(t,e),i=new XMLHttpRequest;i.onload=function(){var t={status:i.status,statusText:i.statusText,headers:c(i.getAllResponseHeaders()||"")};t.url="responseURL"in i?i.responseURL:t.headers.get("X-Request-URL");var e="response"in i?i.response:i.responseText;r(new b(e,t))},i.onerror=function(){o(new TypeError("Network request failed"))},i.ontimeout=function(){o(new TypeError("Network request failed"))},i.open(n.method,n.url,!0),"include"===n.credentials&&(i.withCredentials=!0),"responseType"in i&&m.blob&&(i.responseType="blob"),n.headers.forEach(function(t,e){i.setRequestHeader(e,t)}),i.send("undefined"==typeof n._bodyInit?null:n._bodyInit)})},t.fetch.polyfill=!0}}("undefined"!=typeof self?self:this);
2 | //# sourceMappingURL=fetch.min.js.map
--------------------------------------------------------------------------------
/data_types.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from enum import Enum
3 | from typing import NamedTuple, List, Dict, Union
4 |
5 | from helpers import distance_km, distance, get_iso_time, QUICK_FIX_DIST
6 |
7 |
8 | class UserLoc(NamedTuple):
9 | lat: float
10 | lon: float
11 |
12 |
13 | class BusStop(NamedTuple):
14 | NAME_: str
15 | LAT_: float
16 | LON_: float
17 | ID: int
18 | AZMTH: int = 0
19 |
20 | def __str__(self):
21 | return f'(BusStop: {self.NAME_} {self.LAT_} {self.LON_} )'
22 |
23 | def distance_km(self, bus_stop):
24 | return distance_km(self.LAT_, self.LON_, bus_stop.LAT_, bus_stop.LON_)
25 |
26 |
27 | class LongBusRouteStop(NamedTuple):
28 | NUMBER_: int
29 | NAME_: str
30 | LAT_: float
31 | LON_: float
32 | ROUT_: int
33 | CONTROL_: int = 0
34 | ID: int = 0
35 |
36 | def distance_km(self, bus_stop):
37 | return distance_km(self.LAT_, self.LON_, bus_stop.LAT_, bus_stop.LON_)
38 |
39 |
40 | class ShortBusRoute(NamedTuple):
41 | NUMBER_: int
42 | ROUT_: int
43 | CONTROL_: int
44 | STOPID: int
45 |
46 |
47 | class CoddNextBus(NamedTuple):
48 | rname_: str
49 | time_: int
50 |
51 |
52 | class CoddBus(NamedTuple):
53 | NAME_: str
54 | ID_: int
55 | ROUTE_ACTIVE_: bool = True
56 |
57 |
58 | class CdsBus(NamedTuple):
59 | obj_id_: int
60 | proj_id_: int
61 | last_speed_: int
62 | last_lon_: float
63 | last_lat_: float
64 | name_: str
65 | last_time_: str
66 | route_name_: str
67 | type_proj: int
68 | phone_: str
69 |
70 |
71 | class CoddRouteBus(NamedTuple):
72 | obj_id_: int
73 | proj_id_: int
74 | last_speed_: int
75 | last_lon_: float
76 | last_lat_: float
77 | lon2: int
78 | lat2: int
79 | azimuth: int
80 | last_time_: str
81 | route_name_: str
82 | type_proj: int
83 | lowfloor: int
84 | dist: int = 0
85 |
86 |
87 | class CdsBusPosition(NamedTuple):
88 | lat: float
89 | lon: float
90 | last_time: datetime.datetime
91 |
92 | def distance(self, bus_stop: BusStop = None, user_loc: UserLoc = None):
93 | if not bus_stop and not user_loc:
94 | return QUICK_FIX_DIST
95 | (lat, lon) = (bus_stop.LAT_, bus_stop.LON_) if bus_stop else (user_loc.lat, user_loc.lon)
96 | if lat is None or lon is None:
97 | return QUICK_FIX_DIST
98 | return distance(lat, lon, self.lat, self.lon)
99 |
100 | def distance_km(self, bus_stop: BusStop = None, position: Union[UserLoc, NamedTuple] = None):
101 | (lat, lon) = (bus_stop.LAT_, bus_stop.LON_) if bus_stop else (position.lat, position.lon)
102 | if lat is None or lon is None:
103 | return QUICK_FIX_DIST
104 | return distance_km(lat, lon, self.lat, self.lon)
105 |
106 | def is_valid_coords(self):
107 | return self.lat != 0.0 and self.lon != 0.0
108 |
109 |
110 | class CdsRouteBus(NamedTuple):
111 | last_lat_: float
112 | last_lon_: float
113 | last_speed_: float
114 | last_time_: datetime.datetime
115 | name_: str
116 | obj_id_: int
117 | proj_id_: int
118 | route_name_: str
119 | type_proj: int = 0
120 | last_station_time_: datetime.datetime = None
121 | bus_station_: str = ""
122 | low_floor: bool = False
123 | bus_type: int = 0
124 | obj_output: int = 0
125 | avg_speed: float = 0
126 | avg_last_speed: float = 0
127 | azimuth: int = 0
128 | bort_name: str = ''
129 |
130 | @staticmethod
131 | def make(last_lat_, last_lon_, last_speed_, last_time_, name_, obj_id_, proj_id_, route_name_,
132 | type_proj, last_station_time_, bus_station_, low_floor=False, bus_type=0, avg_speed=18, azimuth=0, bort_name=""):
133 | try:
134 | last_time_ = get_iso_time(last_time_)
135 | last_station_time_ = get_iso_time(last_station_time_) if last_station_time_ else None
136 | except Exception as e:
137 | print(e)
138 | return CdsRouteBus(last_lat_, last_lon_, last_speed_, last_time_, name_, obj_id_, proj_id_,
139 | route_name_, type_proj, last_station_time_, bus_station_, low_floor, bus_type, avg_speed, azimuth, bort_name)
140 |
141 | def get_bus_position(self) -> CdsBusPosition:
142 | return CdsBusPosition(self.last_lat_, self.last_lon_, self.last_time_)
143 |
144 | def filter_by_name(self, filter_query: str) -> bool:
145 | bus_filter = filter_query.lower().split(' ')
146 | name = self.name_.lower()
147 | bort_name = self.bort_name.lower()
148 | return not filter_query or any((q in name for q in bus_filter if q)) or any((q in bort_name for q in bus_filter if q))
149 |
150 | def short(self):
151 | return f'{self.bus_station_}; {self.last_lat_} {self.last_lon_} '
152 |
153 | def distance(self, bus_stop: BusStop = None, user_loc: UserLoc = None):
154 | if not bus_stop and not user_loc:
155 | return QUICK_FIX_DIST
156 | (lat, lon) = (bus_stop.LAT_, bus_stop.LON_) if bus_stop else (user_loc.lat, user_loc.lon)
157 | return distance(lat, lon, self.last_lat_, self.last_lon_)
158 |
159 | def distance_km(self, bus_stop: BusStop = None, user_loc: UserLoc = None):
160 | if not bus_stop and not user_loc:
161 | return QUICK_FIX_DIST
162 | (lat, lon) = (bus_stop.LAT_, bus_stop.LON_) if bus_stop else (user_loc.lat, user_loc.lon)
163 | return distance_km(lat, lon, self.last_lat_, self.last_lon_)
164 |
165 | def is_valid_coords(self):
166 | return self.last_lat_ > 0.0 and self.last_lon_ > 0.0
167 |
168 |
169 | class ArrivalBusStopInfo(NamedTuple):
170 | bus_info: CdsRouteBus
171 | distance: float
172 | time_left: float
173 |
174 |
175 | class ArrivalBusStopInfoFull(NamedTuple):
176 | bus_stop_id: int
177 | bus_stop_name: str
178 | lat: float
179 | lon: float
180 | azmth: int
181 | text: str
182 | bus_routes: List[str] = []
183 | arrival_buses: List[ArrivalBusStopInfo] = []
184 |
185 |
186 | class ArrivalInfo(NamedTuple):
187 | text: str
188 | header: str = ''
189 | arrival_details: List[ArrivalBusStopInfoFull] = []
190 | bus_stops: List[BusStop] = []
191 | found: bool = False
192 |
193 |
194 | class CdsBaseDataProvider:
195 | CACHE_TIMEOUT = 0
196 |
197 | def now(self) -> datetime.datetime:
198 | pass
199 |
200 | def load_all_cds_buses(self) -> List[CdsRouteBus]:
201 | pass
202 |
203 | def load_codd_route_names(self) -> Dict:
204 | pass
205 |
206 | def load_bus_stations_routes(self) -> Dict:
207 | pass
208 |
209 | def load_bus_stops(self) -> List[BusStop]:
210 | pass
211 |
212 | def load_new_codd_route_names(self):
213 | pass
214 |
215 | def load_new_bus_stations_routes(self):
216 | pass
217 |
218 |
219 | class AbuseRule(NamedTuple):
220 | event: Enum
221 | count: int
222 | delta: datetime.timedelta
223 |
224 |
225 | class StatsData(NamedTuple):
226 | min1: int
227 | min10: int
228 | min30: int
229 | min60: int
230 | total: int
231 | text: str
232 |
--------------------------------------------------------------------------------
/helpers.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import logging
4 | import re
5 | import time
6 | from functools import wraps
7 | from itertools import zip_longest, filterfalse
8 | from typing import NamedTuple
9 |
10 | import math
11 | import pytz
12 |
13 | QUICK_FIX_DIST = 10000
14 |
15 | class CustomJsonEncoder(json.JSONEncoder):
16 | def default(self, o):
17 | if isinstance(o, datetime.datetime):
18 | return o.isoformat()
19 | if isinstance(o, set):
20 | return list(o)
21 | return json.JSONEncoder.default(self, o)
22 |
23 |
24 | class SearchResult(NamedTuple):
25 | full_info: bool = False
26 | bus_routes: tuple = tuple()
27 | bus_filter: str = ''
28 | all_buses: bool = False
29 |
30 |
31 | tz = pytz.timezone('Europe/Moscow')
32 | logger = logging.getLogger("vrnbus")
33 |
34 |
35 | def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
36 | return [int(text) if text.isdigit() else text.lower()
37 | for text in re.split(_nsre, s)]
38 |
39 | def sort_routes(source):
40 | routes = list(source)
41 | is_trolley = lambda x: x.startswith("Т")
42 | trolleys = list(filter(is_trolley, routes))
43 | trolleys.sort(key=natural_sort_key)
44 | rest = list(filterfalse(is_trolley, routes))
45 | rest.sort(key=natural_sort_key)
46 | return trolleys + rest
47 |
48 |
49 | def parse_routes(text) -> SearchResult:
50 | if not text:
51 | return SearchResult()._replace(all_buses=True)
52 | if isinstance(text, (list, tuple,)):
53 | text = ' '.join(text)
54 | args = re.split("[ ,;]+", text)
55 | if not args:
56 | return SearchResult()
57 | route_filter = []
58 | bus_filter_start = False
59 | bus_filter = ''
60 | for i in args:
61 | if i in '\/|':
62 | bus_filter_start = True
63 | continue
64 | if bus_filter_start:
65 | bus_filter += i + ' '
66 | continue
67 | route_filter.append(i)
68 | if not route_filter:
69 | return SearchResult(bus_filter=bus_filter, all_buses=True)
70 | route_filter = [x.replace('_', ' ') for x in route_filter]
71 | full_info = route_filter[0].upper() in ['PRO', 'ПРО']
72 | if full_info:
73 | route_filter = route_filter[1:]
74 |
75 | return SearchResult(full_info, tuple(route_filter), bus_filter, not route_filter)
76 |
77 |
78 | def distance(lat1, lon1, lat2, lon2):
79 | if not all((lat1, lon1, lat2, lon2)):
80 | return QUICK_FIX_DIST
81 | return ((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2) ** 0.5
82 |
83 |
84 | def azimuth(glon1, glat1, glon2, glat2):
85 | if not all((glon1, glat1, glon2, glat2)):
86 | return QUICK_FIX_DIST
87 | lat1 = glat1 * math.pi / 180
88 | lat2 = glat2 * math.pi / 180
89 | long1 = glon1 * math.pi / 180
90 | long2 = glon2 * math.pi / 180
91 | cosl1 = math.cos(lat1)
92 | cosl2 = math.cos(lat2)
93 | sinl1 = math.sin(lat1)
94 | sinl2 = math.sin(lat2)
95 | delta = long2 - long1
96 | cdelta = math.cos(delta)
97 | sdelta = math.sin(delta)
98 | x = (cosl1 * sinl2) - (sinl1 * cosl2 * cdelta)
99 | y = sdelta * cosl2
100 | z = (math.atan(-y / x)) / 0.017453293
101 | if (x < 0):
102 | z = z + 180
103 | z2 = (z + 180) % 360 + 180
104 | z2 = -(z2 * 0.017453293)
105 | anglerad2 = z2 - ((2 * math.pi) * math.floor(z2 / (2 * math.pi)))
106 | angledeg = (anglerad2 * 180) / math.pi
107 | azmth = round(angledeg)
108 |
109 | return azmth
110 |
111 |
112 | def parse_int(s, default=0):
113 | try:
114 | return int(s), True
115 | except Exception:
116 | return default, False
117 |
118 |
119 | def distance_km(glat1, glon1, glat2, glon2):
120 | if not all((glat1, glon1, glat2, glon2)):
121 | return QUICK_FIX_DIST
122 | r = 6373.0
123 |
124 | lat1 = math.radians(glat1)
125 | lon1 = math.radians(glon1)
126 | lat2 = math.radians(glat2)
127 | lon2 = math.radians(glon2)
128 |
129 | diff_lon = lon2 - lon1
130 | diff_lat = lat2 - lat1
131 |
132 | a = math.sin(diff_lat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(diff_lon / 2) ** 2
133 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
134 |
135 | result = r * c
136 | return result
137 |
138 |
139 | def get_iso_time(s) -> datetime.datetime:
140 | if isinstance(s, datetime.datetime):
141 | return s
142 |
143 | try:
144 | return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f')
145 | except ValueError:
146 | return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S')
147 |
148 |
149 | def get_time(s):
150 | if isinstance(s, datetime.datetime):
151 | return tz.localize(s)
152 | return tz.localize(datetime.datetime.strptime(s, '%b %d, %Y %I:%M:%S %p'))
153 |
154 |
155 | def grouper(n, iterable, fill_value=None):
156 | """grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"""
157 | args = [iter(iterable)] * n
158 | return zip_longest(fillvalue=fill_value, *args)
159 |
160 |
161 | def retry_multi(max_retries=5):
162 | """ Retry a function `max_retries` times. """
163 |
164 | def retry(func):
165 | @wraps(func)
166 | def wrapper(*args, **kwargs):
167 | num_retries = 0
168 | ret = None
169 | while num_retries <= max_retries:
170 | try:
171 | ret = func(*args, **kwargs)
172 | break
173 | except Exception as e:
174 | logger.exception(e)
175 | if num_retries == max_retries:
176 | raise
177 | num_retries += 1
178 | time.sleep(5)
179 | return ret
180 |
181 | return wrapper
182 |
183 | return retry
184 |
185 |
186 | def fuzzy_search(needle: str, haystack: str) -> bool:
187 | hlen = len(haystack)
188 | nlen = len(needle)
189 | needle = needle.lower()
190 | haystack = haystack.lower()
191 | if nlen > hlen or nlen == 0:
192 | return False
193 | if needle in haystack:
194 | return True
195 | position = 0
196 | for i in range(nlen):
197 | nch = needle[i]
198 | position = haystack.find(nch, position) + 1
199 | if position == 0:
200 | return False
201 | return True
202 |
203 |
204 | def fuzzy_search_advanced(needle: str, haystack: str) -> bool:
205 | hlen = len(haystack)
206 | nlen = len(needle)
207 | needle = needle.lower()
208 | haystack = haystack.lower()
209 | if nlen > hlen or nlen == 0:
210 | return False
211 |
212 | if needle in haystack:
213 | return True
214 |
215 | nch = needle[0]
216 | position = haystack.find(nch)
217 | if position == -1:
218 | return False
219 |
220 | skip_chars = (' ', ',', '(', ')', '.')
221 | if position > 0 and nch not in skip_chars:
222 | while True:
223 | position = haystack.find(nch, position) + 1
224 | if position == 0:
225 | return False
226 | if haystack[position - 2] in skip_chars:
227 | break
228 |
229 | i = 1
230 | while i < nlen:
231 | nch = needle[i]
232 | prev_position = position + 1
233 | position = haystack.find(nch, position) + 1
234 |
235 | if position == 0:
236 | return False
237 |
238 | skip_pos = position - prev_position
239 |
240 | if skip_pos > 0 and haystack[position - 2] not in skip_chars and nch not in skip_chars and i > 1:
241 | i -= 1
242 | continue
243 |
244 | i += 1
245 |
246 | return True
247 |
--------------------------------------------------------------------------------
/test_cds.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import logging
3 | import time
4 | import unittest
5 |
6 | from cds import CdsRequest
7 | from data_processors import WebDataProcessor
8 | from data_providers import CdsTestDataProvider, CdsDBDataProvider
9 | from data_types import CdsBusPosition, CdsRouteBus, BusStop
10 | from helpers import parse_routes
11 | from tracking import EventTracker
12 |
13 | logging.basicConfig(format='%(asctime)s - %(levelname)s [%(filename)s:%(lineno)s %(funcName)20s] %(message)s',
14 | level=logging.INFO,
15 | handlers=[logging.StreamHandler()])
16 |
17 | logger = logging.getLogger("vrnbus")
18 |
19 |
20 | class CdsRouteTestCase(unittest.TestCase):
21 | def __init__(self, *args, **kwargs):
22 | super(CdsRouteTestCase, self).__init__(*args, **kwargs)
23 | self.mock_provider = CdsTestDataProvider(logger)
24 | self.cds = CdsRequest(logger, self.mock_provider)
25 | self.date_time = datetime.datetime(2018, 2, 15, 19, 56, 53)
26 |
27 | def test_routes_on_bus_stop(self):
28 | result = self.cds.get_routes_on_bus_stop(57)
29 | self.assertTrue(result)
30 |
31 | def test_bus_stop_distance(self):
32 | route_name = "5А"
33 | stop_1 = "у-м Молодежный (ул. Лизюкова в центр)"
34 | stop_2 = "ул. Лизюкова (ул. Жукова в центр)"
35 |
36 | with self.subTest('Normal bus station order'):
37 | result = self.cds.get_dist(route_name, stop_1, stop_2)
38 | self.assertTrue(result)
39 |
40 | with self.subTest('Reverse bus station order'):
41 | result = self.cds.get_dist(route_name, stop_2, stop_1)
42 | self.assertFalse(result)
43 |
44 | def test_closest_bus_stop_checked(self):
45 | route_name = '5А'
46 | pos_1 = CdsBusPosition(51.705497, 39.149543, self.date_time) # у-м Молодёжный
47 | pos_2 = CdsBusPosition(51.705763, 39.155278, self.date_time) # 60 лет ВЛКСМ
48 |
49 | with self.subTest('From city center '):
50 | result = self.cds.get_closest_bus_stop_checked(route_name, (pos_2, pos_1))
51 | self.assertTrue(result.NAME_ == 'у-м Молодежный (ул. Лизюкова из центра)')
52 | self.assertEqual(result.NUMBER_, 62)
53 |
54 | with self.subTest('To city center '):
55 | result = self.cds.get_closest_bus_stop_checked(route_name, (pos_1, pos_2))
56 | self.assertEqual(result.NUMBER_, 5)
57 |
58 | def test_closest_bus_stop_same_stations(self):
59 | positions = [CdsBusPosition(51.667033, 39.193648, self.date_time),
60 | CdsBusPosition(51.672135, 39.187541, self.date_time),
61 | CdsBusPosition(51.675065, 39.185286, self.date_time),
62 | CdsBusPosition(51.677922, 39.184953, self.date_time),
63 | CdsBusPosition(51.677922, 39.184953, self.date_time),
64 | CdsBusPosition(51.680843, 39.184798, self.date_time)]
65 |
66 | result = self.cds.get_closest_bus_stop_checked("90", positions)
67 |
68 | self.assertTrue(result.NUMBER_ == 40)
69 | self.assertTrue(result.NAME_ == 'Проспект Труда (Московский проспект из центра)')
70 |
71 | def test_closest_bus_stop(self):
72 | route_bus = CdsRouteBus.make(*[
73 | 51.625537, 39.177478,
74 | 16,
75 | "2018-02-15T19:57:47",
76 | "М617АК136",
77 | 834,
78 | 20,
79 | "80",
80 | 0,
81 | "2018-02-15T19:54:56",
82 | "Рабочий проспект (из центра)"
83 | ])
84 |
85 | station = self.cds.get_closest_bus_stop(route_bus)
86 | logger.info(f"{station}; {route_bus.distance_km(station):.4f} {route_bus.distance(station):.4f}")
87 |
88 |
89 | class CdsDataGatheringTestCase(unittest.TestCase):
90 | def __init__(self, *args, **kwargs):
91 | super(CdsDataGatheringTestCase, self).__init__(*args, **kwargs)
92 | self.mock_provider = CdsTestDataProvider(logger)
93 |
94 | @unittest.skip("testing skipping")
95 | def test_db(self):
96 | self.db_provider = CdsDBDataProvider(logger)
97 | cds = CdsRequest(logger, self.db_provider)
98 | self.call_common_methods(cds)
99 |
100 | def test_mock(self):
101 | cds = CdsRequest(logger, self.mock_provider)
102 | self.call_common_methods(cds)
103 |
104 | def call_common_methods(self, cds):
105 | all_data = cds.load_all_cds_buses_from_db()
106 | cds.calc_avg_speed()
107 |
108 |
109 | class CdsSpeedTestCase(unittest.TestCase):
110 | def __init__(self, *args, **kwargs):
111 | super(CdsSpeedTestCase, self).__init__(*args, **kwargs)
112 | logging.basicConfig(format='%(asctime)s - %(levelname)s [%(filename)s:%(lineno)s %(funcName)20s] %(message)s',
113 | level=logging.INFO,
114 | handlers=[logging.StreamHandler()])
115 |
116 | logger = logging.getLogger("vrnbus")
117 | self.mock_provider = CdsDBDataProvider(logger)
118 | self.cds = CdsRequest(logger, self.mock_provider)
119 |
120 | def test_avg_speed(self):
121 | avg_speeds = []
122 | for i in range(50):
123 | time.sleep(0.005)
124 | self.cds.calc_avg_speed()
125 | avg_speeds.append(f"{self.cds.speed_dict['5А']:.2f} {self.cds.speed_dict['125']:.2f}")
126 | logger.info("\n".join(avg_speeds))
127 |
128 | def test_speed_businfo(self):
129 | query = 'про'
130 | search_request = parse_routes(query)
131 | start = datetime.datetime.now()
132 | result = self.cds.bus_request(search_request, short_format=True)
133 | finish = datetime.datetime.now()
134 | logger.info(f"{finish - start}")
135 |
136 |
137 | class CdsBusStopIndexTestCase(unittest.TestCase):
138 | def __init__(self, *args, **kwargs):
139 | super().__init__(*args, **kwargs)
140 | logging.basicConfig(format='%(asctime)s - %(levelname)s [%(filename)s:%(lineno)s %(funcName)20s] %(message)s',
141 | level=logging.INFO,
142 | handlers=[logging.StreamHandler()])
143 |
144 | logger = logging.getLogger("vrnbus")
145 | self.mock_provider = CdsTestDataProvider(logger)
146 | self.cds = CdsRequest(logger, self.mock_provider)
147 |
148 | def test_get_index_by_name(self):
149 | result = self.cds.get_bus_stop_id("ул. Кирова (в центр)")
150 | self.assertIsInstance(result, int)
151 |
152 | def test_get_index_for_wrong_name(self):
153 | result = self.cds.get_bus_stop_id("NOT A STATION")
154 | self.assertIsInstance(result, int)
155 | self.assertEqual(result, -1)
156 |
157 | def test_get_bus_stop_for_index(self):
158 | result = self.cds.get_bus_stop_from_id(42)
159 | self.assertIsInstance(result, BusStop)
160 |
161 | def test_get_busstop_for_outrange_indexes(self):
162 | self.assertIsNone(self.cds.get_bus_stop_from_id(-1))
163 | self.assertIsNone(self.cds.get_bus_stop_from_id(100500))
164 |
165 | def test_routes_on_near_stations(self):
166 | # TODO: Find the test case for 49A
167 | routes_1 = self.cds.get_routes_on_bus_stop("Политехнический институт (из центра)")
168 | routes_2 = self.cds.get_routes_on_bus_stop("Рабочий проспект (из центра)")
169 | self.assertListEqual(routes_1, routes_2)
170 |
171 |
172 | class CdsBusArrivalTestCases(unittest.TestCase):
173 | def __init__(self, *args, **kwargs):
174 | super().__init__(*args, **kwargs)
175 | logging.basicConfig(format='%(asctime)s - %(levelname)s [%(filename)s:%(lineno)s %(funcName)20s] %(message)s',
176 | level=logging.INFO,
177 | handlers=[logging.StreamHandler()])
178 |
179 | self.logger = logging.getLogger("vrnbus")
180 | self.tracker = EventTracker(logger, [])
181 | self.mock_provider = CdsTestDataProvider(logger)
182 | self.cds = CdsRequest(logger, self.mock_provider)
183 | self.processor = WebDataProcessor(self.cds, self.logger, self.tracker)
184 |
185 | def test_arrival(self):
186 | result = self.processor.get_arrival("про 49 5а", 51.692727, 39.18297)
187 | stops = result['bus_stops']
188 | counts = 0
189 | for k, v in stops.items():
190 | self.logger.info(k)
191 | self.logger.info(v)
192 | counts += len(v.split('\n'))
193 | break
194 | self.logger.info(result)
195 | self.logger.info(counts)
196 |
197 | def test_businfo(self):
198 | result = self.processor.get_bus_info("про 49 5а", 51.692727, 39.18297, True)
199 | self.logger.info(result)
200 |
201 | def test_arrival_distance(self):
202 | src = 'Центральный автовокзал (в центр)'
203 | dst = 'Площадь Застава (в центр)'
204 | wrong_direction = self.cds.get_dist("27", dst, src)
205 | right_direction = self.cds.get_dist("27", src, dst)
206 | self.assertTrue(wrong_direction == 0)
207 | self.assertGreater(right_direction, 0)
208 |
209 | def test_arrival_by_id(self):
210 | for i in range(500):
211 | result = self.processor.get_arrival_by_id("pro", i)
212 | self.assertTrue(result['arrival_info']['found'])
213 |
214 |
215 | if __name__ == '__main__':
216 | unittest.main()
217 |
--------------------------------------------------------------------------------
/data_processors.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import os
4 | import random
5 | from logging import Logger
6 | from typing import List
7 |
8 | import cachetools
9 |
10 | from cds import CdsRequest
11 | from data_types import UserLoc, ArrivalInfo, CdsRouteBus
12 | from db import session_scope
13 | from fotobus_scrapper import fb_links
14 | from helpers import parse_routes
15 | from models import RouteEdges
16 | from tracking import EventTracker
17 |
18 | LOAD_TEST_DATA = False
19 |
20 | try:
21 | import settings
22 |
23 | LOAD_TEST_DATA = settings.LOAD_TEST_DATA
24 | except ImportError:
25 | LOAD_TEST_DATA = os.environ.get('LOAD_TEST_DATA', False)
26 |
27 | ttl_sec = 10 if not LOAD_TEST_DATA else 0.0001
28 |
29 | COMPLAINS_EMAIL = os.environ.get('COMPLAINS_EMAIL', 'МБУ ЦОДД ')
30 |
31 |
32 | def isnamedtupleinstance(x):
33 | _type = type(x)
34 | bases = _type.__bases__
35 | if len(bases) != 1 or bases[0] != tuple:
36 | return False
37 | fields = getattr(_type, '_fields', None)
38 | if not isinstance(fields, tuple):
39 | return False
40 | return all(type(i) == str for i in fields)
41 |
42 |
43 | def unpack_namedtuples(obj):
44 | if isinstance(obj, dict):
45 | return {key: unpack_namedtuples(value) for key, value in obj.items()}
46 | elif isinstance(obj, list):
47 | return [unpack_namedtuples(value) for value in obj]
48 | elif isnamedtupleinstance(obj):
49 | return {key: unpack_namedtuples(value) for key, value in obj._asdict().items()}
50 | elif isinstance(obj, tuple):
51 | return tuple(unpack_namedtuples(value) for value in obj)
52 | else:
53 | return obj
54 |
55 |
56 | def eliminate_numbers(d: dict, full_info, is_fraud) -> dict:
57 | if not full_info:
58 | d['hidden_name'] = d['name_']
59 | d['name_'] = d['bort_name']
60 |
61 | if is_fraud:
62 | d['last_lat_'] += random.uniform(-0.05, 0.05)
63 | d['last_lon_'] += random.uniform(-0.05, 0.05)
64 | d['obj_id_'] = random.uniform(0, 2000)
65 | d['proj_id_'] = random.uniform(0, 2000)
66 | raise Exception("Wrong request")
67 |
68 | return d
69 |
70 |
71 | class BaseDataProcessor:
72 | def __init__(self, cds: CdsRequest, logger: Logger, tracker: EventTracker):
73 | self.cds = cds
74 | self.logger = logger
75 | self.tracker = tracker
76 |
77 |
78 | class WebDataProcessor(BaseDataProcessor):
79 | def __init__(self, cds: CdsRequest, logger: Logger, tracker: EventTracker):
80 | super().__init__(cds, logger, tracker)
81 |
82 | @cachetools.func.ttl_cache(ttl=ttl_sec, maxsize=4096)
83 | def get_bus_info(self, query, lat, lon, full_info, hide_text=True):
84 | user_loc = None
85 | if lat and lon:
86 | user_loc = UserLoc(float(lat), float(lon))
87 | routes_info = parse_routes(query)
88 | is_fraud = not full_info and len(routes_info.bus_routes) > 25
89 |
90 | result = self.cds.bus_request(routes_info, user_loc=user_loc, short_format=True)
91 | return {'q': query,
92 | 'server_time': datetime.datetime.now(),
93 | 'text': '' if hide_text else result[0],
94 | 'buses': [(eliminate_numbers(x[0]._asdict(), full_info, is_fraud),
95 | x[1]._asdict() if x[1] and not is_fraud else {}) for x
96 | in result[1]]}
97 |
98 | @cachetools.func.ttl_cache(ttl=ttl_sec, maxsize=4096)
99 | def get_email_complain(self, query):
100 | routes_info = parse_routes(" pro \ " + query, )
101 | result = self.cds.bus_request(routes_info)
102 | buses = result[1]
103 | self.logger.info(f"{query} {result=}")
104 | if not buses:
105 | return None
106 |
107 | buses_with_bort_name = [x[0] for x in buses if x[0].bort_name == query]
108 | if not buses_with_bort_name:
109 | return None
110 |
111 | bus:CdsRouteBus = buses_with_bort_name[0]
112 | self.logger.info(f"{bus=}")
113 |
114 | subject = f'Обращение с сайта vrnbus:{datetime.date.today()}, маршрут {bus.route_name_}, номер {bus.bort_name}'
115 |
116 | br = '%0D%0A'
117 | body = f"""Обращение с сайта vrnbus: {datetime.date.today()}, маршрут {bus.route_name_}, номер {bus.bort_name}
118 | Дата и время обращения: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
119 | Примерное время и место (уточните, если значительно отличается): {bus.last_station_time_:%Y-%m-%d %H:%M}, {bus.bus_station_}
120 | Обращение: опишите свою жалобу/пожелание/благодарность, при необходимости прикрепите фото/видео, ссылки и т.д.
121 | """.replace("\n", br)
122 |
123 | email_complains = f'mailto:{COMPLAINS_EMAIL}?subject={subject}&body={body}'
124 |
125 | return email_complains
126 |
127 | def get_arrival(self, query, lat, lon):
128 | matches = self.cds.matches_bus_stops(lat, lon)
129 | self.logger.info(f'{lat};{lon} {";".join([str(i) for i in matches])}')
130 | result_tuple = self.cds.next_bus_for_matches(tuple(matches), parse_routes(query))
131 | response = {'lat': lat, 'lon': lon,
132 | 'text': result_tuple[0], 'header': result_tuple[1],
133 | 'server_time': datetime.datetime.now(),
134 | 'bus_stops': {v.bus_stop_name: v.text for v in result_tuple.arrival_details}}
135 | return response
136 |
137 | @cachetools.func.ttl_cache(ttl=ttl_sec)
138 | def get_arrival_by_name(self, query, station_query):
139 | result_tuple = self.cds.next_bus(station_query, parse_routes(query))
140 | if result_tuple.found:
141 | response = {'text': result_tuple[0], 'header': result_tuple[1],
142 | 'server_time': datetime.datetime.now(),
143 | 'bus_stops': {v.bus_stop_name: v.text for v in
144 | result_tuple.arrival_details}}
145 | else:
146 | response = {'text': result_tuple[0], 'header': result_tuple[1],
147 | 'server_time': datetime.datetime.now(),
148 | 'bus_stops': {k: '' for k in
149 | result_tuple.bus_stops}}
150 | return response
151 |
152 | def get_text_from_arrival_info(self, arrival_info: ArrivalInfo):
153 | def text_for_bus_stop(value):
154 | return f"({value.bus_stop_id}) {value.bus_stop_name}\n{value.text}"
155 |
156 | next_bus_text = '\n'.join([text_for_bus_stop(v) for v in arrival_info.arrival_details])
157 | return f'{arrival_info.header}\n{next_bus_text}'
158 |
159 | def get_arrival_by_id(self, query, busstop_id):
160 | bus_stop = self.cds.get_bus_stop_from_id(busstop_id)
161 | if bus_stop:
162 | search_params = parse_routes(query)
163 | arrival_info = self.cds.next_bus_for_matches((bus_stop,), search_params)
164 | result_text = self.get_text_from_arrival_info(arrival_info)
165 | response = {'result': result_text, 'server_time': datetime.datetime.now(),
166 | 'arrival_info': unpack_namedtuples(arrival_info)}
167 | return response
168 |
169 | def get_bus_list(self):
170 | response = {'result': self.cds.codd_buses}
171 | return response
172 |
173 | @cachetools.func.ttl_cache(ttl=36000)
174 | def get_bus_stops(self):
175 | response = {'result': [x._asdict() for x in self.cds.all_bus_stops]}
176 | return response
177 |
178 | @cachetools.func.ttl_cache(ttl=36000)
179 | def get_fotobus_url(self, name):
180 | links = fb_links(name)
181 | return links
182 |
183 | @cachetools.func.ttl_cache(ttl=15)
184 | def get_route_edges(self):
185 | with session_scope(f'Return all RouteEdges') as session:
186 | edges: List[RouteEdges] = session.query(RouteEdges).all()
187 | return [{"edge_key": json.loads(x.edge_key),
188 | "points": json.loads(x.points)} for x in edges]
189 |
190 | def add_route_edges(self, edge_key, points):
191 | with session_scope(f'RouteEdges id {edge_key}') as session:
192 | edge = session.query(RouteEdges).filter_by(edge_key=edge_key).first()
193 | if not edge:
194 | edge = RouteEdges(edge_key=edge_key)
195 | session.add(edge)
196 | edge.points = points
197 | session.commit()
198 |
199 | @cachetools.func.ttl_cache(ttl=36000)
200 | def get_bus_stops_for_routes(self):
201 | response = {route_name: [x._asdict() for x in bus_stops] for (route_name, bus_stops) in
202 | self.cds.bus_routes.items()}
203 | return response
204 |
205 | @cachetools.func.ttl_cache(ttl=36000)
206 | def get_bus_stops_for_routes_for_apps(self):
207 | response = self.cds.bus_routes
208 | return response
209 |
210 | @cachetools.func.ttl_cache(ttl=15)
211 | def get_stats(self):
212 | user_stats = self.tracker.stats()
213 | bus_stats = self.cds.get_bus_statistics()
214 | return str(datetime.datetime.now()) + '\n\n' + user_stats + '\n\n' + bus_stats.text
215 |
216 | @cachetools.func.ttl_cache(ttl=36000)
217 | def get_new_routes(self):
218 | response = {'result': self.cds.codd_new_buses}
219 | return response
220 |
221 | @cachetools.func.ttl_cache(ttl=60)
222 | def get_bus_stops_for_new_routes(self):
223 | response = {route_name: [x._asdict() for x in bus_stops] for (route_name, bus_stops) in
224 | self.cds.get_new_bus_routes().items()}
225 | return response
226 |
227 |
228 | class TelegramDataProcessor(BaseDataProcessor):
229 | def __init__(self, cds: CdsRequest, logger: Logger):
230 | super().__init__(cds, logger)
231 |
--------------------------------------------------------------------------------
/website.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from concurrent.futures import ThreadPoolExecutor
4 | from pathlib import Path
5 |
6 | import tornado.web
7 |
8 | import helpers
9 | from abuse_checker import AbuseChecker
10 | from data_processors import WebDataProcessor
11 | from tracking import WebEvent, EventTracker
12 |
13 | if 'DYNO' in os.environ:
14 | debug = False
15 | else:
16 | debug = True
17 |
18 | FULL_ACCESS_KEY = os.environ.get('FULL_ACCESS_KEY', '')
19 |
20 | try:
21 | import settings
22 |
23 | FULL_ACCESS_KEY = settings.FULL_ACCESS_KEY
24 | except ImportError:
25 | FULL_ACCESS_KEY = os.environ.get('FULL_ACCESS_KEY', '')
26 |
27 |
28 | # noinspection PyAbstractClass
29 | class NoCacheStaticFileHandler(tornado.web.StaticFileHandler):
30 | def set_extra_headers(self, path):
31 | self.set_header("Cache-control", "no-cache")
32 |
33 |
34 | class BaseHandler(tornado.web.RequestHandler):
35 | executor = ThreadPoolExecutor()
36 |
37 | def prepare(self):
38 | if not self.get_cookie("user_ip"):
39 | self.set_cookie("user_ip", self.remote_ip, expires_days=30)
40 |
41 | def track(self, event: WebEvent, *params):
42 | if 'CFNetwork' in self.user_agent:
43 | self.tracker.web(WebEvent.IOS, self.user_ip, *params, self.user_agent)
44 | elif 'Dalvik' in self.user_agent or 'Android' in self.user_agent:
45 | self.tracker.web(WebEvent.ANDROID, self.user_ip, *params, self.user_agent)
46 | else:
47 | self.tracker.web(WebEvent.WEB_SITE, self.user_ip, *params, self.user_agent)
48 |
49 | self.tracker.web(event, self.user_ip, *params, self.user_agent)
50 |
51 | def data_received(self, chunk):
52 | pass
53 |
54 | def set_default_headers(self):
55 | self.set_header("Access-Control-Allow-Origin", "*")
56 | self.set_header("Access-Control-Allow-Headers", "x-requested-with")
57 | self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
58 |
59 | def set_extra_headers(self, _):
60 | self.set_header("Tk", "N")
61 | self.caching()
62 |
63 | def caching(self, max_age=15):
64 | self.set_header("Content-Type", "application/json; charset=utf-8")
65 | self.set_header("Cache-Control", f"max-age={max_age}")
66 |
67 | @property
68 | def user_ip(self):
69 | return self.get_cookie("user_ip", self.remote_ip)
70 |
71 | @property
72 | def remote_ip(self):
73 | return self.request.headers.get('X-Forwarded-For',
74 | self.request.headers.get('X-Real-Ip', self.request.remote_ip))
75 |
76 | @property
77 | def anti_abuser(self):
78 | return self.application.anti_abuser
79 |
80 | @property
81 | def referer(self):
82 | return self.request.headers.get("Referer", "")
83 |
84 | @property
85 | def full_access(self):
86 | return self.referer and FULL_ACCESS_KEY in self.referer
87 |
88 | @property
89 | def user_agent(self):
90 | return self.request.headers["User-Agent"]
91 |
92 | @property
93 | def processor(self) -> WebDataProcessor:
94 | return self.application.processor
95 |
96 | @property
97 | def logger(self):
98 | return self.application.logger
99 |
100 | @property
101 | def tracker(self) -> EventTracker:
102 | return self.application.tracker
103 |
104 | @property
105 | def is_mobile(self):
106 | return any((x in self.user_agent for x in ['CFNetwork', 'Dalvik', 'Android',]))
107 |
108 | class BusSite(tornado.web.Application):
109 | def __init__(self, processor: WebDataProcessor, logger, tracker: EventTracker, anti_abuser: AbuseChecker):
110 | static_handler = tornado.web.StaticFileHandler if not debug else NoCacheStaticFileHandler
111 | handlers = [
112 | (r"/arrival", ArrivalHandler),
113 | (r"/arrival_by_id", ArrivalByIdHandler),
114 | (r"/codd_arrival_by_id", ArrivalByIdHandler),
115 | (r"/busmap", BusInfoHandler),
116 | (r"/businfolist", BusInfoHandler),
117 | (r"/buslist", BusListHandler),
118 | (r"/new_routes", NewRoutesHandler),
119 | (r"/bus_stop_search", BusStopSearchHandler),
120 | (r"/bus_stops_routes", BusStopsRoutesHandler),
121 | (r"/bus_stops_new_routes", BusStopsNewRoutesHandler),
122 | (r"/bus_stations.json", BusStopsRoutesForAppsHandler),
123 | (r"/bus_stops", BusStopsHandler),
124 | (r"/fotobus_info", FotoBusHandler),
125 | (r"/complains", EmailFromBusHandler),
126 | (r"/bus_route_edges", BusRouteEdgesHandler),
127 | (r"/ping", PingHandler),
128 | (r"/(.*.json)", static_handler, {"path": Path("./")}),
129 | (r"/stats.html", StatsHandler),
130 | (r"/(.*)", static_handler, {"path": Path("./fe"), "default_filename": "index.html"}),
131 | ]
132 | tornado.web.Application.__init__(self, handlers, compress_response=True)
133 | self.logger = logger
134 | self.processor = processor
135 | self.tracker = tracker
136 | self.anti_abuser = anti_abuser
137 |
138 |
139 | class PingHandler(BaseHandler):
140 | def get(self):
141 | self.logger.info('PING')
142 | self.write("PONG")
143 | self.caching(max_age=600)
144 |
145 |
146 | class BusInfoHandler(BaseHandler):
147 | def bus_info_response(self, src, query, lat, lon, parent_url, hide_text):
148 | is_map = src == 'map'
149 | if self.referer and 'vrnbus.herokuapp.com' not in self.referer:
150 | self.track(WebEvent.FRAUD, self.referer, query, lat, lon)
151 | if parent_url:
152 | self.track(WebEvent.FRAUD, parent_url, query, lat, lon)
153 | event = WebEvent.BUSMAP if is_map else WebEvent.BUSINFO
154 | if self.user_ip != self.remote_ip:
155 | self.track(WebEvent.IPCHANGE, f'{self.user_ip} != {self.remote_ip}')
156 | if self.full_access:
157 | self.track(WebEvent.FULLINFO, self.referer, query, lat, lon)
158 | if not self.anti_abuser.add_user_event(event, self.user_ip) and not self.full_access:
159 | self.track(WebEvent.ABUSE, query, lat, lon)
160 | return self.send_error(500)
161 | self.track(event, src, query, lat, lon)
162 | response = self.processor.get_bus_info(query, lat, lon, self.full_access, hide_text)
163 | self.write(json.dumps(response, cls=helpers.CustomJsonEncoder))
164 | self.caching()
165 |
166 | def get(self):
167 | q = self.get_argument('q')
168 | src = self.get_argument('src', None)
169 | lat = self.get_argument('lat', None)
170 | lon = self.get_argument('lon', None)
171 | parent_url = self.get_argument('parentUrl', None)
172 | hide_text = self.get_argument('hide_text', None) is not None
173 |
174 | self.bus_info_response(src, q, lat, lon, parent_url, hide_text)
175 |
176 |
177 | class ArrivalHandler(BaseHandler):
178 | def arrival_response(self):
179 | (lat, lon) = (float(self.get_argument(x)) for x in ('lat', 'lon'))
180 | query = self.get_argument('q')
181 | self.track(WebEvent.ARRIVAL, query, lat, lon)
182 | response = self.processor.get_arrival(query, lat, lon)
183 | self.write(json.dumps(response, cls=helpers.CustomJsonEncoder))
184 | self.caching()
185 |
186 | def get(self):
187 | self.arrival_response()
188 |
189 |
190 | class ArrivalByIdHandler(BaseHandler):
191 | def arrival_response(self):
192 | busstop_id = int(self.get_argument('id'))
193 | query = self.get_argument('q', "")
194 | self.track(WebEvent.ARRIVAL, query, busstop_id)
195 | response = self.processor.get_arrival_by_id(query, busstop_id)
196 | self.write(json.dumps(response, cls=helpers.CustomJsonEncoder))
197 | self.caching()
198 |
199 | def get(self):
200 | self.arrival_response()
201 |
202 |
203 | class BusListHandler(BaseHandler):
204 | def _response(self):
205 | response = self.processor.get_bus_list()
206 | self.write(json.dumps(response))
207 | if response:
208 | self.caching(max_age=24 * 60 * 60)
209 |
210 | def get(self):
211 | self._response()
212 |
213 |
214 | class NewRoutesHandler(BaseHandler):
215 | def _response(self):
216 | response = self.processor.get_new_routes()
217 | self.write(json.dumps(response))
218 | if response:
219 | self.caching(max_age=24 * 60 * 60)
220 |
221 | def get(self):
222 | self._response()
223 |
224 |
225 | class BusStopsHandler(BaseHandler):
226 | def _response(self):
227 | response = self.processor.get_bus_stops()
228 | self.write(json.dumps(response, ensure_ascii=False))
229 | if response:
230 | self.caching(max_age=24 * 60 * 60)
231 |
232 | def get(self):
233 | self._response()
234 |
235 | class BusStopsRoutesForAppsHandler(BaseHandler):
236 | def _response(self):
237 | response = self.processor.get_bus_stops_for_routes_for_apps()
238 | self.write(json.dumps(response, ensure_ascii=False, indent=1, cls=helpers.CustomJsonEncoder))
239 | if response:
240 | self.caching(max_age=24 * 60 * 60)
241 |
242 | def get(self):
243 | self._response()
244 |
245 | class BusStopsRoutesHandler(BaseHandler):
246 | def _response(self):
247 | response = self.processor.get_bus_stops_for_routes()
248 | self.write(json.dumps(response, ensure_ascii=False))
249 | if response:
250 | self.caching(max_age=24 * 60 * 60)
251 |
252 | def get(self):
253 | self._response()
254 |
255 |
256 | class BusStopsNewRoutesHandler(BaseHandler):
257 | def _response(self):
258 | response = self.processor.get_bus_stops_for_new_routes()
259 | self.write(json.dumps(response, ensure_ascii=False))
260 | if response:
261 | self.caching(max_age=24 * 60 * 60)
262 |
263 | def get(self):
264 | self._response()
265 |
266 |
267 | class BusStopSearchHandler(BaseHandler):
268 | def _response(self):
269 | query = self.get_argument('q')
270 | station_query = self.get_argument('station')
271 | self.track(WebEvent.BUSSTOP, query, station_query)
272 | response = self.processor.get_arrival_by_name(query, station_query)
273 | self.logger.info(response)
274 | self.write(json.dumps(response, cls=helpers.CustomJsonEncoder))
275 | self.caching()
276 |
277 | def get(self):
278 | self._response()
279 |
280 |
281 | class FotoBusHandler(BaseHandler):
282 | def _response(self):
283 | name = self.get_argument('name')
284 | self.track(WebEvent.FOTOBUS, name)
285 | links = self.processor.get_fotobus_url(name)
286 | if links:
287 | self.redirect(links[0])
288 | else:
289 | self.send_error(404)
290 |
291 | def get(self):
292 | self._response()
293 |
294 |
295 | class StatsHandler(BaseHandler):
296 | def arrival_response(self):
297 | self.track(WebEvent.USER_STATS)
298 | response = self.processor.get_stats()
299 | self.write(response)
300 | self.caching()
301 |
302 | def get(self):
303 | self.arrival_response()
304 |
305 |
306 | class BusRouteEdgesHandler(BaseHandler):
307 | def arrival_response(self):
308 | data = tornado.escape.json_decode(self.request.body)
309 | self.write(data)
310 | self.caching()
311 |
312 | def post(self):
313 | if not self.full_access:
314 | self.send_error(401, reason="Wrong the page URL")
315 | return
316 | data = tornado.escape.json_decode(self.request.body)
317 | edge_key = json.dumps(data.get("edge_key"))
318 | points = json.dumps(data.get("points"))
319 | self.processor.add_route_edges(edge_key, points)
320 |
321 | def get(self):
322 | result = self.processor.get_route_edges()
323 | self.write(json.dumps(result))
324 | self.caching()
325 |
326 |
327 | class EmailFromBusHandler(BaseHandler):
328 | def _response(self):
329 | bort_number = self.get_argument('bort_number')
330 | test = self.get_argument('test', None)
331 | self.track(WebEvent.COMPLAIN, bort_number)
332 | complain = self.processor.get_email_complain(bort_number)
333 | if not complain:
334 | self.write(f'Автобус с бортовым номером {bort_number} на линии не найден')
335 | return
336 |
337 | if test:
338 | txt_complain = complain.replace('%0D%0A', '
\n').replace('&', '&
\n')
339 | self.write(f'{txt_complain}')
340 | else:
341 | self.redirect(complain)
342 |
343 | def get(self):
344 | self._response()
--------------------------------------------------------------------------------
/fe/busroutes.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | "use strict";
3 | var map;
4 | var marker_group;
5 | var my_renderer;
6 | var coords = {latitude: 51.6754966, longitude: 39.2088823}
7 |
8 | var timer_id = 0
9 | var timer_stop_id = 0
10 |
11 | var bus_route_stops = []
12 | var edited_edges = {}
13 | var bus_stop_auto_complete
14 | var drawn_items
15 |
16 | var route_run = document.getElementById('route_run')
17 | var current_route = []
18 | var icon_urls = []
19 | var current_bus_stop_id = 0
20 | var show_old_routes = false
21 |
22 |
23 | document.getElementById('route_stop').onclick = function(){
24 | clearTimeout(timer_id)
25 | timer_id = 0
26 | timer_stop_id = 0
27 | }
28 |
29 | document.getElementById('show_old_routes').onclick = function(cb){
30 | show_old_routes = cb.currentTarget.checked
31 | update_route_info(show_old_routes)
32 | }
33 |
34 | function run_timer(func) {
35 | if (!timer_id) {
36 | timer_id = setTimeout(function tick() {
37 | func().then(function () {
38 | timer_id = setTimeout(tick, 1 * 500)
39 | }).catch(function(){
40 | clearTimeout(timer_id)
41 | timer_id = 0
42 | timer_stop_id = 0
43 | })
44 | }, 1 * 100)
45 | if (!timer_stop_id)
46 | timer_stop_id = setTimeout(function () {
47 | clearTimeout(timer_id)
48 | timer_id = 0
49 | timer_stop_id = 0
50 |
51 | }, 2* 60 * 1000)
52 | }
53 | }
54 |
55 | route_run.onclick = function () {
56 | run_timer(function(){
57 | if (!current_route || !current_route.length){
58 | return Promise.resolve()
59 | }
60 | marker_group.clearLayers()
61 |
62 | var busnumber = document.getElementById('busnumber').value;
63 | var busspeed = document.getElementById('busspeed').value;
64 | var wait_time = document.getElementById('wait_time').value;
65 |
66 | var bus_stop_interval = Math.floor(current_route.length / busnumber)
67 |
68 | if (current_bus_stop_id >= current_route.length){
69 | current_bus_stop_id = 0;
70 | }
71 | current_bus_stop_id++
72 |
73 | for(var i=0; i < busnumber; i++){
74 | var curr_id = (current_bus_stop_id + i * bus_stop_interval) % current_route.length
75 | if (curr_id >= current_route.length){
76 | curr_id = 0;
77 | }
78 |
79 | var bus_stop = current_route[curr_id]
80 | console.log('bus_stop ', bus_stop)
81 | add_png_marker(bus_stop, i)
82 | .addTo(marker_group)
83 | .bindTooltip(i + ' ' + bus_stop.NAME_, {permanent: true})
84 | }
85 | return Promise.resolve()
86 | })
87 | }
88 |
89 | function add_png_marker(item, index) {
90 | var shadowUrl = 'https://unpkg.com/leaflet@1.3.3/dist/images/marker-shadow.png'
91 |
92 | var icon = new L.Icon({
93 | iconUrl: icon_urls[index % icon_urls.length],
94 | shadowUrl: shadowUrl,
95 | iconSize: [25, 41],
96 | iconAnchor: [12, 41],
97 | popupAnchor: [1, -34],
98 | shadowSize: [41, 41]
99 | });
100 |
101 | return L.marker([item.LAT_, item.LON_],
102 | {
103 | icon: icon
104 | }
105 | )
106 | }
107 |
108 | function get_bus_stops_routes(old_routes) {
109 | if (bus_route_stops.length > 0) {
110 | return update_bus_stops_routes(bus_route_stops)
111 | }
112 |
113 | return fetch(old_routes ? '/bus_stops_routes' : '/bus_stops_new_routes',
114 | {
115 | method: 'GET',
116 | headers: {
117 | 'Content-Type': 'application/json'
118 | },
119 | })
120 | .then(function (res) {
121 | return res.json()
122 | })
123 | .then(function (data) {
124 | bus_route_stops = data
125 | // update_bus_stops_routes(data)
126 | })
127 | .then(function () {
128 | return fetch('/bus_route_edges', {
129 | method: 'GET'})
130 | }).then(function (res) {
131 | return res.json()
132 | }).
133 | then(function (data) {
134 | data.forEach(function (item) {
135 | edited_edges[item.edge_key] = item.points
136 | })
137 | })
138 | }
139 |
140 |
141 |
142 | function get_bus_list(old_routes) {
143 | return fetch(old_routes ? '/buslist' : '/new_routes',
144 | {
145 | method: 'GET',
146 | headers: {
147 | 'Content-Type': 'application/json'
148 | },
149 | credentials: 'include',
150 | })
151 | .then(function (res) {
152 | return res.json()
153 | })
154 | .then(function (data) {
155 | var bus_list = data.result
156 |
157 | var select = document.getElementById('bus_routes')
158 | while (select.options.length > 0) {
159 | select.remove(0);
160 | }
161 | select.appendChild(new Option('Все маршруты', ''))
162 | bus_list.forEach(function (bus_name) {
163 | var opt = new Option(bus_name, bus_name)
164 | select.appendChild(opt)
165 | })
166 |
167 | select.onchange = function () {
168 | var route_name = select.options[select.selectedIndex].value;
169 | if (route_name || document.getElementById('show_all_routes').checked){
170 | update_bus_stops_routes(bus_route_stops, route_name)
171 | }
172 | }
173 |
174 | document.getElementById('busnumber').onchange = select.onchange;
175 | document.getElementById('busspeed').onchange = select.onchange;
176 | document.getElementById('show_all_routes').onchange = select.onchange;
177 | document.getElementById('show_old_routes').onchange = select.onchange;
178 | document.getElementById('wait_time').onchange = select.onchange;
179 | })
180 | }
181 |
182 |
183 | function update_bus_stops_routes(bus_stops_routes, selected_route_name) {
184 | drawn_items.clearLayers()
185 | var my_renderer = L.canvas({padding: 0.5});
186 | var edges = {}
187 | var bus_stops = {}
188 | var wrong_stops = []
189 | var route_by_edges = {}
190 |
191 | var natural_collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
192 |
193 |
194 |
195 | for (var route_name in bus_stops_routes) {
196 | if (!route_name)
197 | continue
198 | if (selected_route_name && route_name !== selected_route_name)
199 | continue
200 | var route = bus_stops_routes[route_name]
201 | current_route = bus_stops_routes[route_name]
202 | var curr_point = route[0]
203 | route.forEach(function (item) {
204 | if (curr_point === item) {
205 | return
206 | }
207 |
208 | var edge_key = [curr_point.ID, item.ID]
209 | var edge_info = `( ${edge_key} ) ${curr_point.NAME_} - ${item.NAME_}`
210 | if (!(edge_key in route_by_edges)){
211 | route_by_edges[edge_key] = []
212 | }
213 |
214 | route_by_edges[edge_key].push(route_name)
215 | route_by_edges[edge_key].sort(natural_collator.compare)
216 |
217 | var edge = edges[edge_key]
218 | var routes = route_by_edges[edge_key]
219 | var popup_content = `${edge_info}
` + routes.join('
')
220 |
221 | if (edge_key in edges) {
222 | edge.setPopupContent(popup_content)
223 | curr_point = item
224 | return
225 | }
226 |
227 | if (curr_point.NUMBER_ === item.NUMBER_) {
228 | console.log(edge_info)
229 | }
230 |
231 | var pointA = new L.LatLng(curr_point.LAT_, curr_point.LON_);
232 | var pointB = new L.LatLng(item.LAT_, item.LON_);
233 | var pointList = [pointA, pointB];
234 | if (edited_edges[edge_key]){
235 | pointList = edited_edges[edge_key];
236 | }
237 |
238 | var firstpolyline = new L.Polyline(pointList, {
239 | color: 'blue',
240 | weight: 5,
241 | opacity: 0.5,
242 | smoothFactor: 1,
243 | edge_key: edge_key,
244 | edge_info: edge_info
245 | }).bindPopup(popup_content);
246 | // firstpolyline.addTo(map);
247 | drawn_items.addLayer(firstpolyline)
248 |
249 | edges[edge_key] = firstpolyline
250 |
251 | curr_point = item
252 |
253 | })
254 | }
255 |
256 | if (selected_route_name) {
257 | var length_route = 0;
258 | var previousPoint = null;
259 | Object.values(edges).forEach((polyline) => {
260 | polyline.getLatLngs().forEach(function (latLng) {
261 | if (previousPoint) {
262 | length_route += previousPoint.distanceTo(latLng)/1000
263 | }
264 | previousPoint = latLng;
265 | });
266 | })
267 | var routeinfo = document.getElementById('routeinfo');
268 | var busnumber = document.getElementById('busnumber').value;
269 | var busspeed = document.getElementById('busspeed').value;
270 | var wait_time = document.getElementById('wait_time').value;
271 | var minute_interval = ((length_route + (wait_time/60)*busspeed)/busnumber)*60/busspeed;
272 | routeinfo.innerText = `${selected_route_name} - ${length_route.toFixed(2)} км, ${busnumber}, ${minute_interval.toFixed(2)} минут `
273 | }
274 | }
275 |
276 | function save_to_ls(key, value) {
277 | if (!ls_test()) {
278 | return
279 | }
280 | localStorage.setItem(key, value)
281 | }
282 |
283 | function load_from_ls(key) {
284 | if (!ls_test()) {
285 | return
286 | }
287 | return localStorage.getItem(key)
288 | }
289 |
290 | function ls_test() {
291 | var test = 'test'
292 | if (!'localStorage' in window) {
293 | return false
294 | }
295 | try {
296 | localStorage.setItem(test, test)
297 | localStorage.removeItem(test)
298 | return true
299 | } catch (e) {
300 | return false
301 | }
302 | }
303 |
304 | function update_route_info(old_routes) {
305 | drawn_items.clearLayers()
306 | get_bus_list(old_routes)
307 | get_bus_stops_routes(old_routes)
308 | }
309 |
310 | function init() {
311 | map = L.map('mapid', {
312 | fullscreenControl: {
313 | pseudoFullscreen: true // if true, fullscreen to page width and height
314 | },
315 | minZoom: 10,
316 | maxZoom: 22
317 | }).setView([51.6754966, 39.2088823], 11)
318 |
319 | my_renderer = L.canvas({padding: 0.5});
320 |
321 | L.tileLayer('https://b.tile.openstreetmap.org/{z}/{x}/{y}.png', {
322 | attribution: '© OpenStreetMap contributors',
323 | minZoom: 10,
324 | maxZoom: 22,
325 | maxNativeZoom: 18
326 | }).addTo(map)
327 |
328 | // FeatureGroup is to store editable layers
329 | drawn_items = new L.FeatureGroup();
330 | map.addLayer(drawn_items);
331 |
332 | map.addControl(new L.Control.Draw({
333 | edit: {
334 | featureGroup: drawn_items,
335 | poly: {
336 | allowIntersection: true
337 | }
338 | },
339 | draw: {
340 | polygon: {
341 | allowIntersection: true,
342 | showArea: true
343 | }
344 | }
345 | }));
346 |
347 |
348 | var iconUrls = [
349 | 'marker-icon-2x-blue.png',
350 | 'marker-icon-2x-green.png',
351 | 'marker-icon-2x-red.png',
352 | ]
353 |
354 | var base_color_marker_url = 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/'
355 | iconUrls.forEach(function (value) {
356 | icon_urls.push(base_color_marker_url + value)
357 | })
358 |
359 | map.on("draw:created", function (event) {
360 | var layer = event.layer;
361 | });
362 |
363 | map.on('draw:edited', function (e) {
364 | var layers = e.layers;
365 | layers.eachLayer(function (edited_layer) {
366 | var edge_key = edited_layer.options.edge_key;
367 | var points = edited_layer.editing.latlngs[0];
368 | edited_edges[edited_layer.options.edge_key] = points;
369 | fetch('/bus_route_edges', {
370 | method: 'POST', // или 'PUT'
371 | body: JSON.stringify({
372 | 'edge_key': edge_key,
373 | 'points': points
374 | }), // данные могут быть 'строкой' или {объектом}!
375 | headers: {'Content-Type': 'application/json'},
376 | credentials: 'include',
377 | })
378 | .then(res => res.ok ? res : Promise.reject(res))
379 | .catch(error => {
380 | console.error(error)
381 | alert(`Ошибка при редактировании маршрута: ${error.status} ${error.statusText} `)
382 | })
383 | });
384 | });
385 |
386 | marker_group = L.layerGroup().addTo(map);
387 | L.control.ruler().addTo(map);
388 |
389 | update_route_info()
390 | }
391 |
392 | document.addEventListener("DOMContentLoaded", init);
393 | })()
--------------------------------------------------------------------------------
/fe/main.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | "use strict";
3 |
4 | if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
5 | location.href = 'https:' + window.location.href.substring(window.location.protocol.length);
6 | }
7 |
8 | var coords = {latitude: 51.6754966, longitude: 39.2088823}
9 |
10 | var lastbusquery = document.getElementById('lastbusquery')
11 | var station_query = document.getElementById('station_query')
12 | var station_name = document.getElementById('station_name')
13 |
14 | var timer_id = 0
15 | var timer_stop_id = 0
16 |
17 | var info = document.getElementById('info')
18 | var businfo = document.getElementById('businfo')
19 | var lastbus = document.getElementById('lastbus')
20 | var nextbus_loading = document.getElementById('nextbus_loading')
21 | var lastbus_loading = document.getElementById('lastbus_loading')
22 | var cb_refresh = document.getElementById('cb_refresh')
23 | var cb_show_info = document.getElementById('cb_show_info')
24 | var btn_station_search = document.getElementById('btn_station_search')
25 |
26 | var bus_stop_list = []
27 | var bus_stop_names = []
28 | var bus_stop_auto_complete
29 |
30 | if (lastbus)
31 | lastbus.onclick = function () {
32 | get_cds_bus()
33 | }
34 |
35 | if (cb_refresh)
36 | cb_refresh.onclick = function () {
37 | if (!cb_refresh.checked) {
38 | clearTimeout(timer_id)
39 | clearTimeout(timer_stop_id)
40 | timer_id = 0
41 | timer_stop_id = 0
42 | }
43 | }
44 |
45 | if (cb_show_info) {
46 | cb_show_info.onclick = function () {
47 | var show = cb_show_info.checked
48 | businfo.className = show ? "" : "hide_info"
49 | }
50 | }
51 |
52 | if (btn_station_search) {
53 | btn_station_search.onclick = function () {
54 | run_search_by_name()
55 | }
56 | }
57 | if (lastbusquery) {
58 | lastbusquery.onkeyup = function (event) {
59 | event.preventDefault()
60 | if (event.keyCode === 13) {
61 | get_cds_bus()
62 | }
63 | }
64 | }
65 |
66 | if (station_query) {
67 | station_query.onkeyup = function (event) {
68 | event.preventDefault()
69 | if (event.keyCode === 13) {
70 | run_search_by_name()
71 | }
72 | }
73 | }
74 |
75 | if (station_name) {
76 | station_name.onkeyup = function (event) {
77 | event.preventDefault()
78 | if (event.keyCode === 13) {
79 | run_search_by_name()
80 | }
81 | }
82 | }
83 |
84 | function run_timer(func) {
85 | if (cb_refresh.checked && !timer_id) {
86 | timer_id = setTimeout(function tick() {
87 | func().then(function () {
88 | if (cb_refresh.checked)
89 | timer_id = setTimeout(tick, 30 * 1000)
90 | })
91 | }, 30 * 1000)
92 | if (!timer_stop_id)
93 | timer_stop_id = setTimeout(function () {
94 | cb_refresh.checked = false
95 | clearTimeout(timer_id)
96 | timer_id = 0
97 | timer_stop_id = 0
98 |
99 | }, 10 * 60 * 1000)
100 | }
101 | }
102 |
103 | function setCookie(name, value, options) {
104 | options = options || {};
105 |
106 | var expires = options.expires;
107 |
108 | if (typeof expires == "number" && expires) {
109 | var d = new Date();
110 | d.setTime(d.getTime() + expires * 1000);
111 | expires = options.expires = d;
112 | }
113 | if (expires && expires.toUTCString) {
114 | options.expires = expires.toUTCString();
115 | }
116 |
117 | value = encodeURIComponent(value);
118 |
119 | var updatedCookie = name + "=" + value;
120 |
121 | for (var propName in options) {
122 | updatedCookie += "; " + propName;
123 | var propValue = options[propName];
124 | if (propValue !== true) {
125 | updatedCookie += "=" + propValue;
126 | }
127 | }
128 |
129 | document.cookie = updatedCookie;
130 | }
131 |
132 | function getCookie(name) {
133 | var matches = document.cookie.match(new RegExp(
134 | "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
135 | ));
136 | return matches ? decodeURIComponent(matches[1]) : undefined;
137 | }
138 |
139 | function run_search_by_name() {
140 | run_timer(run_search_by_name)
141 | return get_bus_arrival_by_name()
142 | }
143 |
144 | function get_cds_bus() {
145 | run_timer(get_cds_bus)
146 |
147 | var bus_query = lastbusquery.value
148 | save_to_ls('bus_query', bus_query)
149 | return get_bus_positions(bus_query)
150 | }
151 |
152 | function update_user_position() {
153 | if ("geolocation" in navigator) {
154 | navigator.geolocation.getCurrentPosition(function (position) {
155 | coords = position.coords
156 | })
157 | }
158 | }
159 |
160 | if ("geolocation" in navigator) {
161 | var nextbus = document.getElementById('nextbus')
162 |
163 | if (nextbus)
164 | nextbus.onclick = function (event) {
165 | event.preventDefault()
166 | get_current_pos(get_bus_arrival)
167 | }
168 | }
169 |
170 | function save_station_params() {
171 | var query = station_query.value
172 | var station = station_name.value
173 |
174 | save_to_ls('station_query', query)
175 | save_to_ls('station', station)
176 | }
177 |
178 | function get_current_pos(func) {
179 | save_station_params()
180 |
181 | navigator.geolocation.getCurrentPosition(func)
182 | }
183 |
184 | function format_bus_stops(header, bus_stops) {
185 | var bus_stop_info = header + '\n'
186 | for (var prop in bus_stops) {
187 | bus_stop_info += '' + prop + '' + '\n' + bus_stops[prop] + '\n'
188 | }
189 |
190 | info.innerHTML = bus_stop_info
191 | var elements = document.getElementsByClassName('bus_linked')
192 | for (var i = 0; i < elements.length; i++) {
193 | elements[i].onclick = function (e) {
194 | e.preventDefault()
195 | if (e.srcElement && e.srcElement.text) {
196 | station_name.value = e.srcElement.text
197 | get_bus_arrival_by_name()
198 | }
199 | }
200 | }
201 | }
202 |
203 | function get_bus_arrival_by_name() {
204 | var btn_station_search = document.getElementById('btn_station_search')
205 | waiting(nextbus_loading, btn_station_search, true)
206 |
207 | var bus_query = station_query.value
208 | var station = station_name.value
209 |
210 | save_station_params()
211 |
212 | var params = 'q=' + encodeURIComponent(bus_query) +
213 | '&station=' + encodeURIComponent(station)
214 |
215 | return fetch('/bus_stop_search?' + params,
216 | {
217 | method: 'GET',
218 | headers: {
219 | 'Content-Type': 'application/json'
220 | },
221 | credentials: 'include',
222 | })
223 | .then(function (res) {
224 | return res.json()
225 | })
226 | .then(function (data) {
227 | update_cookies()
228 | waiting(nextbus_loading, btn_station_search, false)
229 | format_bus_stops(data.header, data.bus_stops)
230 | })
231 | .catch(function (error) {
232 | waiting(nextbus_loading, btn_station_search, false)
233 | info.innerHTML = 'Ошибка: ' + error
234 | })
235 | }
236 |
237 | function get_bus_arrival(position) {
238 | var nextbus = document.getElementById('nextbus')
239 | waiting(nextbus_loading, nextbus, true)
240 |
241 | coords = position.coords
242 | var bus_query = station_query.value
243 |
244 | var params = 'q=' + encodeURIComponent(bus_query) +
245 | '&lat=' + encodeURIComponent(coords.latitude) +
246 | '&lon=' + encodeURIComponent(coords.longitude)
247 |
248 | return fetch('/arrival?' + params,
249 | {
250 | method: 'GET',
251 | headers: {
252 | 'Content-Type': 'application/json'
253 | },
254 | credentials: 'include',
255 | })
256 | .then(function (res) {
257 | return res.json()
258 | })
259 | .then(function (data) {
260 | update_cookies()
261 | waiting(nextbus_loading, nextbus, false)
262 | format_bus_stops(data.header, data.bus_stops)
263 | })
264 | .catch(function (error) {
265 | waiting(nextbus_loading, nextbus, false)
266 | info.innerHTML = 'Ошибка: ' + error
267 | })
268 | }
269 |
270 |
271 | function waiting(element, button, state) {
272 | element.className = state ? 'spinner' : ''
273 | button.disabled = state
274 | }
275 |
276 | function fraud_check() {
277 | if (parent !== window){
278 | return "&parentUrl=" + encodeURIComponent(document.referrer)
279 | }
280 | return ""
281 | }
282 |
283 | function get_bus_positions(query) {
284 | waiting(lastbus_loading, lastbus, true)
285 |
286 | var params = 'q=' + encodeURIComponent(query) + fraud_check()
287 | if (coords) {
288 | params += '&lat=' + encodeURIComponent(coords.latitude)
289 | params += '&lon=' + encodeURIComponent(coords.longitude)
290 | }
291 |
292 | return fetch('/businfolist?' + params,
293 | {
294 | method: 'GET',
295 | headers: {
296 | 'Content-Type': 'application/json'
297 | },
298 | credentials: 'include',
299 | })
300 | .then(function (res) {
301 | return res.json()
302 | })
303 | .then(function (data) {
304 | update_cookies()
305 | waiting(lastbus_loading, lastbus, false)
306 | var q = data.q
307 | var text = data.text
308 | businfo.innerHTML = 'Маршруты: ' + q + '\nКоличество результатов: ' + data.buses.length + '\n' + text
309 | }).catch(function (error) {
310 | waiting(lastbus_loading, lastbus, false)
311 |
312 | businfo.innerHTML = 'Ошибка: ' + error
313 | })
314 | }
315 |
316 | function get_bus_stop_list() {
317 | return fetch('/bus_stops',
318 | {
319 | method: 'GET',
320 | headers: {
321 | 'Content-Type': 'application/json'
322 | },
323 | })
324 | .then(function (res) {
325 | return res.json()
326 | })
327 | .then(function (data) {
328 | update_cookies()
329 | bus_stop_list = data.result
330 | bus_stop_names = bus_stop_list.map(function callback(bus_stop) {
331 | return bus_stop.NAME_
332 | })
333 | if (station_name) {
334 | bus_stop_auto_complete = new autoComplete({
335 | selector: station_name,
336 | source: function (term, suggest) {
337 | term = term.toLowerCase();
338 | var matches = [];
339 | for (var i = 0; i < bus_stop_names.length; i++)
340 | if (~bus_stop_names[i].toLowerCase().indexOf(term)) matches.push(bus_stop_names[i]);
341 | suggest(matches);
342 | }
343 | })
344 | }
345 | })
346 | }
347 |
348 | function update_cookies() {
349 | var user_ip = getCookie("user_ip")
350 | if (user_ip) {
351 | save_to_ls("user_ip", user_ip)
352 | }
353 | }
354 |
355 | function get_bus_list() {
356 | return fetch('/buslist',
357 | {
358 | method: 'GET',
359 | headers: {
360 | 'Content-Type': 'application/json'
361 | },
362 | credentials: 'include',
363 | })
364 | .then(function (res) {
365 | return res.json()
366 | })
367 | .then(function (data) {
368 | update_cookies()
369 | var bus_list = data.result
370 |
371 | var select = document.getElementById('buslist')
372 | select.appendChild(new Option('Маршруты', '-'))
373 | bus_list.forEach(function (bus_name) {
374 | var opt = new Option(bus_name, bus_name)
375 | select.appendChild(opt)
376 | })
377 |
378 | select.onchange = function () {
379 | var text = select.options[select.selectedIndex].value; // Текстовое значение для выбранного option
380 | if (text !== '-') {
381 | if (lastbusquery)
382 | lastbusquery.value += ' ' + text
383 | if (station_query)
384 | station_query.value += ' ' + text
385 | }
386 | }
387 | })
388 | }
389 |
390 | function save_to_ls(key, value) {
391 | if (!ls_test()) {
392 | return
393 | }
394 | localStorage.setItem(key, value)
395 | }
396 |
397 | function load_from_ls(key) {
398 | if (!ls_test()) {
399 | return
400 | }
401 | return localStorage.getItem(key)
402 | }
403 |
404 | function ls_test() {
405 | var test = 'test'
406 | if (!'localStorage' in window) {
407 | return false
408 | }
409 | try {
410 | localStorage.setItem(test, test)
411 | localStorage.removeItem(test)
412 | return true
413 | } catch (e) {
414 | return false
415 | }
416 | }
417 |
418 | function init() {
419 | var user_ip = getCookie("user_ip")
420 | var ls_user_ip = load_from_ls('user_ip')
421 | if (!user_ip && ls_user_ip) {
422 | setCookie("user_ip", ls_user_ip, {expires: 3600 * 24 * 7})
423 | }
424 | if (station_name) {
425 | get_bus_stop_list()
426 | }
427 | get_bus_list()
428 |
429 | if (lastbusquery)
430 | lastbusquery.value = load_from_ls('bus_query') || ''
431 |
432 | if (station_query)
433 | station_query.value = load_from_ls('station_query') || ''
434 |
435 | if (station_name)
436 | station_name.value = load_from_ls('station') || ''
437 | }
438 |
439 | document.addEventListener("DOMContentLoaded", init);
440 | })()
--------------------------------------------------------------------------------
/fe/busstops.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | "use strict";
3 |
4 | if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
5 | location.href = 'https:' + window.location.href.substring(window.location.protocol.length);
6 | }
7 |
8 | var l_map;
9 | var marker_group;
10 | var my_renderer;
11 | var coords = {latitude: 51.6754966, longitude: 39.2088823}
12 |
13 | var lastbusquery = document.getElementById('lastbusquery')
14 | var station_query = document.getElementById('station_query')
15 | var station_name = document.getElementById('station_name')
16 |
17 | var timer_id = 0
18 | var timer_stop_id = 0
19 |
20 | var info = document.getElementById('info')
21 | var businfo = document.getElementById('businfo')
22 | var lastbus = document.getElementById('lastbus')
23 | var nextbus_loading = document.getElementById('nextbus_loading')
24 | var lastbus_loading = document.getElementById('lastbus_loading')
25 | var cb_show_labels = document.getElementById('cb_show_labels')
26 | var cb_show_id_only = document.getElementById('cb_show_id_only')
27 | var cb_show_png_markers = document.getElementById('cb_show_png_markers')
28 | var cb_show_info = document.getElementById('cb_show_info')
29 | var cb_animation = document.getElementById('cb_animation')
30 | var btn_station_search = document.getElementById('btn_station_search')
31 |
32 | var bus_stop_list = []
33 | var bus_stop_auto_complete
34 |
35 | if (lastbus)
36 | lastbus.onclick = function () {
37 | get_cds_bus()
38 | }
39 |
40 | if (cb_show_labels) {
41 | cb_show_labels.onclick = function () {
42 | get_bus_stop_list()
43 | }
44 | }
45 |
46 | if (cb_show_id_only) {
47 | cb_show_id_only.onclick = function () {
48 | get_bus_stop_list()
49 | }
50 | }
51 |
52 | if (cb_show_png_markers) {
53 | cb_show_png_markers.onclick = function () {
54 | get_bus_stop_list()
55 | }
56 | }
57 | if (cb_show_info) {
58 | cb_show_info.onclick = function () {
59 | var show = cb_show_info.checked
60 | businfo.className = show ? "" : "hide_info"
61 | }
62 | }
63 |
64 | if (btn_station_search) {
65 | btn_station_search.onclick = function () {
66 | run_search_by_name()
67 | }
68 | }
69 | if (lastbusquery) {
70 | lastbusquery.onkeyup = function (event) {
71 | event.preventDefault()
72 | if (event.keyCode === 13) {
73 | get_cds_bus()
74 | }
75 | }
76 | }
77 |
78 | if (station_query) {
79 | station_query.onkeyup = function (event) {
80 | event.preventDefault()
81 | if (event.keyCode === 13) {
82 | run_search_by_name()
83 | }
84 | }
85 | }
86 |
87 | function run_timer(func) {
88 | if (cb_show_labels.checked && !timer_id) {
89 | timer_id = setTimeout(function tick() {
90 | func().then(function () {
91 | if (cb_show_labels.checked)
92 | timer_id = setTimeout(tick, 30 * 1000)
93 | })
94 | }, 30 * 1000)
95 | if (!timer_stop_id)
96 | timer_stop_id = setTimeout(function () {
97 | cb_show_labels.checked = false
98 | clearTimeout(timer_id)
99 | timer_id = 0
100 | timer_stop_id = 0
101 |
102 | }, 10 * 60 * 1000)
103 | }
104 | }
105 |
106 | function setCookie(name, value, options) {
107 | options = options || {};
108 |
109 | var expires = options.expires;
110 |
111 | if (typeof expires === "number" && expires) {
112 | var d = new Date();
113 | d.setTime(d.getTime() + expires * 1000);
114 | expires = options.expires = d;
115 | }
116 | if (expires && expires.toUTCString) {
117 | options.expires = expires.toUTCString();
118 | }
119 |
120 | value = encodeURIComponent(value);
121 |
122 | var updatedCookie = name + "=" + value;
123 |
124 | for (var propName in options) {
125 | updatedCookie += "; " + propName;
126 | var propValue = options[propName];
127 | if (propValue !== true) {
128 | updatedCookie += "=" + propValue;
129 | }
130 | }
131 |
132 | document.cookie = updatedCookie;
133 | }
134 |
135 | function getCookie(name) {
136 | var matches = document.cookie.match(new RegExp(
137 | "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
138 | ));
139 | return matches ? decodeURIComponent(matches[1]) : undefined;
140 | }
141 |
142 | function update_user_position() {
143 | if ("geolocation" in navigator) {
144 | navigator.geolocation.getCurrentPosition(function (position) {
145 | coords = position.coords
146 | })
147 | }
148 | }
149 |
150 | if ("geolocation" in navigator) {
151 | var nextbus = document.getElementById('nextbus')
152 |
153 | if (nextbus)
154 | nextbus.onclick = function (event) {
155 | event.preventDefault()
156 | get_current_pos(get_bus_arrival)
157 | }
158 | }
159 |
160 | function save_station_params() {
161 | var query = station_query.value
162 | var station = station_name.value
163 |
164 | save_to_ls('station_query', query)
165 | save_to_ls('station', station)
166 | }
167 |
168 | function get_current_pos(func) {
169 | save_station_params()
170 |
171 | navigator.geolocation.getCurrentPosition(func)
172 | }
173 |
174 | function format_bus_stops(header, bus_stops) {
175 | var bus_stop_info = header + '\n'
176 | for (var prop in bus_stops) {
177 | bus_stop_info += '' + prop + '' + '\n' + bus_stops[prop] + '\n'
178 | }
179 |
180 | info.innerHTML = bus_stop_info
181 | var elements = document.getElementsByClassName('bus_linked')
182 | for (var i = 0; i < elements.length; i++) {
183 | elements[i].onclick = function (e) {
184 | e.preventDefault()
185 | if (e.srcElement && e.srcElement.text) {
186 | station_name.value = e.srcElement.text
187 | get_bus_arrival_by_name()
188 | }
189 | }
190 | }
191 | }
192 |
193 | function waiting(element, button, state) {
194 | element.className = state ? 'spinner' : ''
195 | button.disabled = state
196 | }
197 |
198 |
199 | function get_bus_stop_list() {
200 | var show_labels = cb_show_labels.checked;
201 | var show_id_only = cb_show_id_only.checked;
202 | var show_png_markers = cb_show_png_markers.checked;
203 |
204 | if (bus_stop_list.length > 0) {
205 | return update_bus_stops(bus_stop_list, show_labels, show_id_only, show_png_markers)
206 | }
207 |
208 | return fetch('/bus_stops',
209 | {
210 | method: 'GET',
211 | headers: {
212 | 'Content-Type': 'application/json'
213 | },
214 | })
215 | .then(function (res) {
216 | return res.json()
217 | })
218 | .then(function (data) {
219 | update_cookies()
220 | bus_stop_list = data.result
221 |
222 | update_bus_stops_autocomplete(bus_stop_list, show_labels)
223 | update_bus_stops(bus_stop_list)
224 | })
225 | }
226 |
227 | function update_bus_stops_autocomplete(bus_stop_list) {
228 | update_cookies()
229 |
230 | if (!station_name) {
231 | return;
232 | }
233 |
234 | bus_stop_auto_complete = new autoComplete({
235 | selector: station_name,
236 | minChars: 1,
237 | source: function (term, suggest) {
238 | term = term.toLowerCase();
239 | var id = parseInt(term, 10);
240 | var matches = [];
241 | for (var i = 0; i < bus_stop_list.length; i++) {
242 | var bus_stop = bus_stop_list[i]
243 | var name_with_id = bus_stop.ID + " " + bus_stop.NAME_
244 | if (Number.isInteger(id) && id.toString(10) === term) {
245 | if (id === bus_stop.ID) {
246 | matches.push(bus_stop);
247 | }
248 | } else if (~name_with_id.toLowerCase().indexOf(term)) {
249 | matches.push(bus_stop);
250 | }
251 | }
252 | suggest(matches);
253 | },
254 | renderItem: function (item, search) {
255 | search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
256 | var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi");
257 | var name_with_id = item.ID + " " + item.NAME_
258 | var data_values = 'data-lat="' + item.LAT_ + '" data-lng="' + item.LON_ + '"'
259 | return ' '
260 | + name_with_id.replace(re, "$1") + '
';
261 | },
262 | onSelect: function (e, term, item) {
263 | var lat = item.getAttribute('data-lat')
264 | var lng = item.getAttribute('data-lng')
265 | if (cb_animation.checked)
266 | l_map.flyTo([lat, lng], 17)
267 | else
268 | l_map.setView([lat, lng], 17)
269 | }
270 | })
271 | }
272 |
273 | function add_circle_marker(item) {
274 | var marker_colors = ["#3388ff",
275 | "#330088",
276 | "#ff662e"]
277 |
278 | return L.circleMarker([item.LAT_, item.LON_], {
279 | renderer: my_renderer,
280 | fill: true,
281 | fillOpacity: 0.9,
282 | color: "#3388ff"
283 | }).on('click', function (e) {
284 |
285 | var color_index = marker_colors.indexOf(e.target.options.color) + 1
286 | if (color_index >= marker_colors.length) {
287 | color_index = 0
288 | }
289 |
290 | e.target.setStyle({
291 | color: marker_colors[color_index]
292 | })
293 | });
294 | }
295 |
296 | var icon_urls = []
297 |
298 | function add_png_marker(item) {
299 | var shadowUrl = 'https://unpkg.com/leaflet@1.3.3/dist/images/marker-shadow.png'
300 |
301 | var icon = new L.Icon({
302 | iconUrl: icon_urls[0],
303 | shadowUrl: shadowUrl,
304 | iconSize: [25, 41],
305 | iconAnchor: [12, 41],
306 | popupAnchor: [1, -34],
307 | shadowSize: [41, 41]
308 | });
309 |
310 | return L.marker([item.LAT_, item.LON_],
311 | {
312 | icon: icon
313 | }
314 | ).on('click', function (e) {
315 |
316 | var icon_index = icon_urls.indexOf(e.target.options.icon.options.iconUrl) + 1
317 | if (icon_index >= icon_urls.length) {
318 | icon_index = 0
319 | }
320 |
321 | var icon = new L.Icon({
322 | iconUrl: icon_urls[icon_index],
323 | shadowUrl: shadowUrl,
324 | iconSize: [25, 41],
325 | iconAnchor: [12, 41],
326 | popupAnchor: [1, -34],
327 | shadowSize: [41, 41]
328 | });
329 |
330 | this.setIcon(icon)
331 | })
332 | }
333 |
334 |
335 | function update_bus_stops(bus_stop_list, show_tooltips_always, show_id_only, show_png_markers) {
336 | marker_group.clearLayers()
337 | var wrong_stops = []
338 |
339 | bus_stop_list.forEach(function (item) {
340 | if (!item.LAT_ || !item.LON_ || item.LON_ < 30 || item.LAT_ < 30) {
341 | wrong_stops.push(item);
342 | return
343 | }
344 |
345 | var tooltip_text = show_id_only ? "" + item.ID : item.ID + " " + item.NAME_;
346 | var marker = show_png_markers ? add_png_marker(item) : add_circle_marker(item)
347 |
348 | marker
349 | .addTo(marker_group)
350 | .bindTooltip(tooltip_text, {permanent: show_tooltips_always})
351 | })
352 |
353 | var wrong_stops_info = "Проверьте координаты остановок:
"
354 | wrong_stops.forEach(function (item) {
355 | wrong_stops_info += item.ID + " " + item.NAME_ + " (" + item.LAT_ + ", " + item.LON_ + ") " + "
"
356 | })
357 | businfo.innerHTML = wrong_stops_info
358 | }
359 |
360 | function update_cookies() {
361 | var user_ip = getCookie("user_ip")
362 | if (user_ip) {
363 | save_to_ls("user_ip", user_ip)
364 | }
365 | }
366 |
367 | function save_to_ls(key, value) {
368 | if (!ls_test()) {
369 | return
370 | }
371 | localStorage.setItem(key, value)
372 | }
373 |
374 | function load_from_ls(key) {
375 | if (!ls_test()) {
376 | return
377 | }
378 | return localStorage.getItem(key)
379 | }
380 |
381 | function ls_test() {
382 | var test = 'test'
383 | if (!'localStorage' in window) {
384 | return false
385 | }
386 | try {
387 | localStorage.setItem(test, test)
388 | localStorage.removeItem(test)
389 | return true
390 | } catch (e) {
391 | return false
392 | }
393 | }
394 |
395 | function init() {
396 | var user_ip = getCookie("user_ip")
397 | var ls_user_ip = load_from_ls('user_ip')
398 | if (!user_ip && ls_user_ip) {
399 | setCookie("user_ip", ls_user_ip, {expires: 3600 * 24 * 7})
400 | }
401 |
402 |
403 | if (station_name)
404 | station_name.value = load_from_ls('station') || ''
405 |
406 | l_map = L.map('mapid', {
407 | fullscreenControl: {
408 | pseudoFullscreen: true // if true, fullscreen to page width and height
409 | }
410 | }).setView([51.6754966, 39.2088823], 13)
411 |
412 | my_renderer = L.canvas({padding: 0.5});
413 |
414 | L.tileLayer('https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', {
415 | attribution: '© OpenStreetMap contributors',
416 | }).addTo(l_map)
417 |
418 | marker_group = L.layerGroup().addTo(l_map);
419 |
420 | // var myIcon = L.divIcon({className: '',
421 | // html: '
' +
422 | // '
' +
423 | // ' HELLO
'
424 | // });
425 | // // you can set .my-div-icon styles in CSS
426 | // L.marker([51.6754966, 39.2088823], {icon: myIcon}).addTo(l_map);
427 |
428 | var iconUrls = [
429 | 'marker-icon-2x-blue.png',
430 | 'marker-icon-2x-green.png',
431 | 'marker-icon-2x-red.png',
432 | ]
433 |
434 | var base_color_marker_url = 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/'
435 | iconUrls.forEach(function (value) {
436 | icon_urls.push(base_color_marker_url + value)
437 | })
438 |
439 | if (station_name) {
440 | get_bus_stop_list()
441 | }
442 | }
443 |
444 | document.addEventListener("DOMContentLoaded", init);
445 | })()
--------------------------------------------------------------------------------
/data_providers.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | import pathlib
5 | import random
6 | import time
7 | from datetime import datetime
8 | from pathlib import Path
9 | from typing import List, Dict
10 |
11 | import fdb
12 | from firebird.driver import connect, driver_config, transaction, Cursor
13 |
14 | import firebird.driver
15 |
16 | from data_types import CdsRouteBus, CdsBaseDataProvider, CoddBus, LongBusRouteStop, BusStop
17 |
18 | LOAD_TEST_DATA = False
19 |
20 | logger = logging.getLogger(__name__)
21 |
22 | if not Path('dumps').exists():
23 | Path('dumps').mkdir(parents=True)
24 |
25 | try:
26 | import settings
27 |
28 | CDS_HOST = settings.CDS_HOST
29 | CDS_DB_PROJECTS_PATH = settings.CDS_DB_PROJECTS_PATH
30 | CDS_DB_DATA_PATH = settings.CDS_DB_DATA_PATH
31 | CDS_USER = settings.CDS_USER
32 | CDS_PASS = settings.CDS_PASS
33 | LOAD_TEST_DATA = settings.LOAD_TEST_DATA
34 | except ImportError:
35 | settings = None
36 | env = os.environ
37 | if all((x in env for x in ("CDS_HOST", "CDS_DB_PROJECTS_PATH", "CDS_DB_DATA_PATH", "CDS_USER", "CDS_PASS",))):
38 | CDS_HOST = env['CDS_HOST']
39 | CDS_DB_PROJECTS_PATH = env['CDS_DB_PROJECTS_PATH']
40 | CDS_DB_DATA_PATH = env['CDS_DB_DATA_PATH']
41 | CDS_USER = env['CDS_USER']
42 | CDS_PASS = env['CDS_PASS']
43 | else:
44 | LOAD_TEST_DATA = True
45 |
46 |
47 |
48 | driver_config.server_defaults.host.value = CDS_HOST
49 |
50 |
51 | def fetch_cursor_map(cur: Cursor):
52 | cursor_mapping = [x[0] for x in cur.description]
53 | all_rows = cur.fetchall()
54 |
55 | return [{k: v for k, v in zip(cursor_mapping, row)} for row in all_rows]
56 |
57 |
58 | def dump_data(data, resource):
59 | fname = pathlib.Path(f'dumps/{resource}.json')
60 | dump_info = json.dumps(data, default=str)
61 | file_content = dump_info.encode('utf-8')
62 | with open(fname, 'wb+') as f:
63 | f.write(file_content)
64 | logger.info(f'new_dump_file {fname}')
65 |
66 |
67 | def load_data(resource):
68 | fname = pathlib.Path(f'dumps/{resource}.json')
69 | if not fname.exists():
70 | return None
71 | mtime = datetime.fromtimestamp(fname.stat().st_mtime)
72 | mdelta = (datetime.now()-mtime).seconds
73 | if mdelta > 600:
74 | return None
75 | with open(fname, 'rb') as f:
76 | return json.load(f)
77 |
78 |
79 | class CdsDBDataProvider(CdsBaseDataProvider):
80 | CACHE_TIMEOUT = 30
81 |
82 | def __init__(self, logger):
83 | self.logger = logger
84 | self.cds_db_project = connect(database=CDS_DB_PROJECTS_PATH, user=CDS_USER,
85 | password=CDS_PASS, charset='WIN1251')
86 | self.cds_db_data = connect(database=CDS_DB_DATA_PATH, user=CDS_USER,
87 | password=CDS_PASS, charset='WIN1251')
88 | self.cds_db_project.default_tpb = fdb.ISOLATION_LEVEL_READ_COMMITED_RO
89 | self.cds_db_data.default_tpb = fdb.ISOLATION_LEVEL_READ_COMMITED_RO
90 | self.load_obl_objects = False
91 |
92 | def try_reconnect(self):
93 | try:
94 | self.cds_db_project.close()
95 | self.cds_db_project = connect(database=CDS_DB_PROJECTS_PATH, user=CDS_USER,
96 | password=CDS_PASS, charset='WIN1251')
97 | self.cds_db_project.default_tpb = fdb.ISOLATION_LEVEL_READ_COMMITED_RO
98 | self.logger.info(f"Success connect to {CDS_HOST} {CDS_DB_PROJECTS_PATH}")
99 | except Exception as general_error:
100 | self.logger.error(general_error)
101 |
102 | try:
103 | self.cds_db_data.close()
104 | self.cds_db_data = connect(database=CDS_DB_DATA_PATH, user=CDS_USER,
105 | password=CDS_PASS, charset='WIN1251')
106 | self.cds_db_data.default_tpb = fdb.ISOLATION_LEVEL_READ_COMMITED_RO
107 | self.logger.info(f"Success connect to {CDS_HOST} {CDS_DB_DATA_PATH}")
108 | except Exception as general_error:
109 | self.logger.error(general_error)
110 |
111 | def now(self) -> datetime:
112 | return datetime.now()
113 |
114 | def load_codd_route_names(self) -> Dict:
115 | def unpack_data(r):
116 | r = [CoddBus(**x) for x in r]
117 | return {x.NAME_: x for x in r}
118 |
119 | self.logger.debug('Execute fetch routes from DB')
120 | start = time.time()
121 |
122 | cached_result = load_data('codd_route')
123 | if cached_result:
124 | self.logger.info(f"Finish proccess. Elapsed: {time.time() - start:.2f}")
125 | return unpack_data(cached_result)
126 |
127 | try:
128 | with transaction(self.cds_db_project.transaction_manager()) as tr:
129 | cur = tr.cursor()
130 | cur.execute('''select ID_, NAME_, ROUTE_ACTIVE_ from ROUTS
131 | order by NAME_''')
132 | self.logger.debug('Finish execution')
133 | result = fetch_cursor_map(cur)
134 | # tr.commit()
135 | # cur.close()
136 | self.logger.info(f"Finish fetch data. Elapsed: {time.time() - start:.2f}")
137 | except firebird.driver.DatabaseError as db_error:
138 | self.logger.error(db_error)
139 | self.try_reconnect()
140 | return {}
141 |
142 | dump_data(result, 'codd_route')
143 | self.logger.info(f"Finish proccess. Elapsed: {time.time() - start:.2f}")
144 | return unpack_data(result)
145 |
146 | def load_new_codd_route_names(self):
147 | def unpack_data(r):
148 | r = [CoddBus(**x) for x in r]
149 | return {x.NAME_: x.ID_ for x in r}
150 | self.logger.debug('Execute fetch routes from DB')
151 | start = time.time()
152 | cached_result = load_data('new_codd_route')
153 | if cached_result:
154 | self.logger.info(f"Finish proccess. Elapsed: {time.time() - start:.2f}")
155 | return unpack_data(cached_result)
156 | try:
157 | with transaction(self.cds_db_project.transaction_manager()) as tr:
158 | cur = tr.cursor()
159 | cur.execute('''select "Id" as ID_, "Name" as NAME_ from "NewRoute"
160 | where "NewRouteStatusID" <> 3
161 | order by NAME_''')
162 | self.logger.debug('Finish execution')
163 | result = fetch_cursor_map(cur)
164 | self.logger.info(f"Finish fetch data. Elapsed: {time.time() - start:.2f}")
165 | except firebird.driver.DatabaseError as db_error:
166 | self.logger.error(db_error)
167 | self.try_reconnect()
168 | return {}
169 |
170 | dump_data(result, 'new_codd_route')
171 | self.logger.info(f"Finish proccess. Elapsed: {time.time() - start:.2f}")
172 | return unpack_data(result)
173 |
174 | def convert_to_stations_dict(self, bus_routes, bus_stops_data):
175 | bus_routes_ids = {v: k for k, v in bus_routes.items()}
176 | long_bus_stops = [LongBusRouteStop(**x) for x in bus_stops_data]
177 | bus_stations = {}
178 |
179 | for stop in long_bus_stops:
180 | route_name = bus_routes_ids.get(stop.ROUT_, '')
181 | bus_list = bus_stations.get(route_name, [])
182 | bus_list.append(stop)
183 | bus_stations[route_name] = bus_list
184 |
185 | for (k, v) in bus_stations.items():
186 | v.sort(key=lambda tup: tup.NUMBER_)
187 |
188 | return bus_stations
189 |
190 | def load_bus_stations_routes(self) -> Dict:
191 | def unpack_data(r):
192 | routes_dict = {k: v.ID_ for k, v in self.load_codd_route_names().items()}
193 | return self.convert_to_stations_dict(routes_dict, r)
194 | start = time.time()
195 |
196 | cached_result = load_data('bus_stations_routes')
197 | if cached_result:
198 | self.logger.info(f"Finish proccess. Elapsed: {time.time() - start:.2f}")
199 | return unpack_data(cached_result)
200 | try:
201 | with transaction(self.cds_db_project.transaction_manager()) as tr:
202 | cur = tr.cursor()
203 | cur.execute('''select bsr.NUM as NUMBER_, bs.NAME as NAME_, bs.LAT as LAT_,
204 | bs.LON as LON_, bsr.ROUTE_ID as ROUT_, 0 as CONTROL_, bsr.BS_ID as ID
205 | from ROUTS r
206 | join BS_ROUTE bsr on bsr.ROUTE_ID = r.ID_
207 | left join BS on bsr.BS_ID = bs.ID''')
208 | bus_stops_data = fetch_cursor_map(cur)
209 | self.logger.info(f"Finish fetch data. Elapsed: {time.time() - start:.2f}")
210 | except firebird.driver.DatabaseError as db_error:
211 | self.logger.error(db_error)
212 | self.try_reconnect()
213 | return {}
214 |
215 | dump_data(bus_stops_data, 'bus_stations_routes')
216 | self.logger.info(f"Finish proccess. Elapsed: {time.time() - start:.2f}")
217 | return unpack_data(bus_stops_data)
218 |
219 | def load_new_bus_stations_routes(self) -> Dict:
220 | start = time.time()
221 | try:
222 | with transaction(self.cds_db_project.transaction_manager()) as tr:
223 | cur = tr.cursor()
224 | cur.execute('''select bsr."Num" as NUMBER_, nbs."Name" as NAME_, nbs."Latitude" as LAT_,
225 | nbs."Longitude" as LON_,
226 | bsr."RouteId" as ROUT_, 0 as CONTROL_,
227 | bsr."BusStationId" as ID
228 | from "NewRoute" r
229 | join "NewBusStationRoute" bsr on bsr."RouteId" = r."Id"
230 | left join "NewBusStation" nbs on bsr."BusStationId" = nbs."Id"
231 | ''')
232 | bus_stops_data = fetch_cursor_map(cur)
233 | end = time.time()
234 | self.logger.info(f"Finish fetch data. Elapsed: {end - start:.2f}")
235 | except firebird.driver.DatabaseError as db_error:
236 | self.logger.error(db_error)
237 | self.try_reconnect()
238 | return {}
239 |
240 | result = self.convert_to_stations_dict(self.load_new_codd_route_names(), bus_stops_data)
241 | return result
242 |
243 | def load_all_cds_buses(self) -> List[CdsRouteBus]:
244 | def make_names_lower(x):
245 | return {k.lower(): v for (k, v) in x.items()}
246 |
247 | self.logger.debug('Execute fetch all from DB')
248 | start = time.time()
249 |
250 | try:
251 | with transaction(self.cds_db_project.transaction_manager()) as tr:
252 | cur = tr.cursor()
253 | cur.execute('''SELECT bs.NAME_ AS BUS_STATION_, rt.NAME_ AS ROUTE_NAME_, o.NAME_, o.OBJ_ID_, o.LAST_TIME_,
254 | o.LAST_LON_, o.LAST_LAT_, o.LAST_SPEED_, o.LAST_STATION_TIME_, o.PROJ_ID_,
255 | coalesce(o.lowfloor, 0) as low_floor, coalesce(o.VEHICLE_TYPE_, 0) as bus_type,
256 | coalesce(obj_output_, 0) as obj_output,
257 | coalesce(azmth_, 0) as azimuth,
258 | coalesce(o."BortName", '') as bort_name
259 | FROM OBJECTS O LEFT JOIN BUS_STATIONS bs
260 | ON o.LAST_ROUT_ = bs.ROUT_ AND o.LAST_STATION_ = bs.NUMBER_
261 | LEFT JOIN ROUTS rt ON o.LAST_ROUT_ = rt.ID_''')
262 | self.logger.debug('Finish execution')
263 | result = fetch_cursor_map(cur)
264 | end = time.time()
265 | self.logger.info(f"Finish fetch data. Elapsed: {end - start:.2f}")
266 | except (AssertionError, firebird.driver.types.DatabaseError, firebird.driver.DatabaseError) as db_error:
267 | self.logger.error(db_error)
268 | self.try_reconnect()
269 | return []
270 |
271 | obl_result = []
272 | try:
273 | if self.load_obl_objects:
274 | with transaction(self.cds_db_data.transaction_manager())as tr:
275 | cur = tr.cursor()
276 | cur.execute('''SELECT bs.NAME AS BUS_STATION_, rt.NAME_ AS ROUTE_NAME_, o.block_number as OBJ_ID_,
277 | CAST(o.block_number as VARCHAR(10)) as NAME_, o.LAST_TIME as LAST_TIME_,
278 | o.LON as LAST_LON_, o.LAT as LAST_LAT_, 0 as LAST_SPEED_, NULL as LAST_STATION_TIME_, NULL as PROJ_ID_,
279 | 0 as low_floor,
280 | 0 as bus_type,
281 | 0 as obj_output,
282 | coalesce(o.azimuth, 0) as azimuth,
283 | coalesce(o."BortName", '') as bort_name
284 | FROM OBL_OBJECTS O LEFT JOIN BS
285 | ON o.bs_id = bs.ID
286 | LEFT JOIN ROUTS rt ON o.route_id = rt.ID_''')
287 | self.logger.debug('Finish execution')
288 | obl_result = fetch_cursor_map(cur)
289 | end = time.time()
290 | self.logger.info(f"Finish fetch data. Elapsed: {end - start:.2f}")
291 | except firebird.driver.DatabaseError as db_error:
292 | if self.load_obl_objects:
293 | self.load_obl_objects = False
294 | self.logger.error(db_error)
295 | self.try_reconnect()
296 |
297 | result = [CdsRouteBus(**make_names_lower(x)) for x in result + obl_result]
298 | result.sort(key=lambda s: s.last_time_, reverse=True)
299 | end = time.time()
300 | self.logger.info(f"Finish proccess. Elapsed: {end - start:.2f}")
301 | return result
302 |
303 | def load_bus_stops(self) -> List[BusStop]:
304 | self.logger.debug('Execute load_bus_stops from DB')
305 | start = time.time()
306 | try:
307 | with transaction(self.cds_db_project.transaction_manager()) as tr:
308 | cur = tr.cursor()
309 | cur.execute('''select distinct ID, NAME as NAME_, LAT as LAT_, LON as LON_, AZMTH
310 | from bs
311 | order by NAME_''')
312 | self.logger.debug('Finish execution')
313 | result = fetch_cursor_map(cur)
314 | end = time.time()
315 | self.logger.info(f"Finish fetch data. Elapsed: {end - start:.2f}")
316 | except firebird.driver.DatabaseError as db_error:
317 | self.logger.error(db_error)
318 | self.try_reconnect()
319 | return []
320 |
321 | return [BusStop(**x) for x in result]
322 |
323 |
324 | class CdsTestDataProvider(CdsBaseDataProvider):
325 | CACHE_TIMEOUT = 0.0001
326 |
327 | def __init__(self, logger):
328 | self.logger = logger
329 | self.test_data_files = []
330 | self.test_data_index = 0
331 | self.mocked_now = datetime.now()
332 | self.load_test_data()
333 |
334 | def load_test_data(self):
335 | self.test_data_files = sorted(Path('./test_data/').glob('codd_data_db*.json'))
336 | self.test_data_index = 0
337 | if self.test_data_files:
338 | path = self.test_data_files[0]
339 | self.mocked_now = datetime.strptime(path.name, "codd_data_db%y_%m_%d_%H_%M_%S.json")
340 | else:
341 | raise Exception("Cannot load test data from ./test_data/")
342 |
343 | def load_codd_route_names(self) -> Dict:
344 | my_file = Path("bus_routes_codd.json")
345 | with open(my_file, 'rb') as f:
346 | return json.load(f)
347 |
348 | def now(self):
349 | if self.test_data_files and self.test_data_index >= len(self.test_data_files):
350 | self.test_data_index = 0
351 | path = self.test_data_files[self.test_data_index]
352 | self.mocked_now = datetime.strptime(path.name, "codd_data_db%y_%m_%d_%H_%M_%S.json")
353 | return self.mocked_now
354 |
355 | def next_test_data(self):
356 | if self.test_data_files and self.test_data_index >= len(self.test_data_files):
357 | self.test_data_index = 0
358 | path = self.test_data_files[self.test_data_index]
359 | self.mocked_now = datetime.strptime(path.name, "codd_data_db%y_%m_%d_%H_%M_%S.json")
360 | with open(path, 'rb') as f:
361 | long_bus_stops = [CdsRouteBus.make(*i) for i in json.load(f)]
362 | self.test_data_index += 1
363 | self.logger.info(f'Loaded {path.name}; {self.mocked_now:%H:%M:%S}')
364 | return long_bus_stops
365 |
366 | def load_all_cds_buses(self) -> List[CdsRouteBus]:
367 | return self.next_test_data()
368 |
369 | def load_bus_stations_routes(self) -> Dict:
370 | with open(Path("bus_stations.json"), 'rb') as f:
371 | bus_stations = json.load(f)
372 |
373 | result = {}
374 | for k, v in bus_stations.items():
375 | route = [LongBusRouteStop(*i) for i in v]
376 | route.sort(key=lambda tup: tup.NUMBER_)
377 | result[k] = route
378 |
379 | return result
380 |
381 | def load_bus_stops(self) -> List[BusStop]:
382 | with open('bus_stops.json', 'rb') as f:
383 | return [BusStop(**i) for i in json.load(f)]
384 |
385 |
386 | class StubDataProvider(CdsBaseDataProvider):
387 | CACHE_TIMEOUT = 0
388 |
389 | def now(self) -> datetime:
390 | return datetime.now()
391 |
392 | def load_all_cds_buses(self) -> List[CdsRouteBus]:
393 | return []
394 |
395 | def load_codd_route_names(self) -> Dict:
396 | return {}
397 |
398 | def load_bus_stations_routes(self) -> Dict:
399 | return {}
400 |
401 | def load_bus_stops(self) -> List[BusStop]:
402 | return []
403 |
404 | def get_data_provider(logger):
405 | try:
406 | return CdsTestDataProvider(logger) if LOAD_TEST_DATA else CdsDBDataProvider(logger)
407 | except Exception as ex:
408 | logger.exception(ex)
409 | return StubDataProvider()
410 |
411 |
--------------------------------------------------------------------------------
/tgbot.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import datetime
3 | import os
4 | import re
5 | import textwrap
6 |
7 | from apscheduler.schedulers.background import BackgroundScheduler
8 | from telegram import ReplyKeyboardRemove, InlineKeyboardButton, InlineKeyboardMarkup, \
9 | KeyboardButton, ReplyKeyboardMarkup
10 | from telegram.ext import CommandHandler, CallbackQueryHandler, Filters, MessageHandler, Updater, run_async
11 |
12 | from data_types import UserLoc, ArrivalInfo, StatsData, BusStop
13 | from fotobus_scrapper import fb_links
14 | from helpers import parse_routes, natural_sort_key, grouper, SearchResult, parse_int
15 | from tracking import EventTracker, TgEvent, get_event_by_name
16 |
17 | try:
18 | import settings
19 |
20 | VRNBUSBOT_TOKEN = settings.VRNBUSBOT_TOKEN
21 | PING_HOST = settings.PING_HOST
22 | USERS_TO_INFORM = settings.USERS_TO_INFORM
23 | except ImportError:
24 | env = os.environ
25 | VRNBUSBOT_TOKEN = env.get('VRNBUSBOT_TOKEN')
26 | USERS_TO_INFORM = env.get('USERS_TO_INFORM', "")
27 | PING_HOST = env.get('PING_HOST', 'http://localhost:8088')
28 |
29 |
30 | class BusBot:
31 | def __init__(self, cds, user_settings, logger, tracker: EventTracker):
32 | """Start the bot."""
33 | self.cds = cds
34 | self.user_settings = user_settings
35 | self.logger = logger
36 | self.tracker = tracker
37 | # Create the EventHandler and pass it your bot's token.
38 | if not VRNBUSBOT_TOKEN:
39 | self.logger.error("The Telegram bot token is empty. Use @BotFather to get your token")
40 | return
41 | self.stats_fail_start = None
42 | self.stats_fail_start_last = None
43 | self.users_to_inform = [int(x.strip()) for x in USERS_TO_INFORM.split(",")] if USERS_TO_INFORM else []
44 | self.logger.info(f"User to inform in Tg: {self.users_to_inform}")
45 | self.updater = Updater(VRNBUSBOT_TOKEN, request_kwargs={'read_timeout': 10})
46 | self.bot = self.updater.bot
47 | # Get the dispatcher to register handlers
48 | self.dp = self.updater.dispatcher
49 |
50 | # on different commands - answer in Telegram
51 | self.updater.dispatcher.add_handler(CommandHandler('settings', self.settings, pass_args=True))
52 | self.updater.dispatcher.add_handler(CallbackQueryHandler(self.settings_button))
53 | self.dp.add_handler(CommandHandler("start", self.start))
54 |
55 | self.dp.add_handler(CommandHandler("help", self.helpcmd))
56 | self.dp.add_handler(CommandHandler("last", self.last_buses, pass_args=True))
57 |
58 | self.dp.add_handler(CommandHandler("nextbus", self.next_bus_handler, pass_args=True))
59 | self.dp.add_handler(CommandHandler("fb", self.fb_link_handler, pass_args=True))
60 |
61 | self.dp.add_handler(CommandHandler("userstats", self.user_stats))
62 | self.dp.add_handler(CommandHandler("userstatspro", self.user_stats_pro, pass_args=True))
63 |
64 | self.dp.add_handler(CommandHandler("stats", self.stats))
65 | self.dp.add_handler(CommandHandler("statspro", self.stats_full))
66 | #
67 | # # on noncommand i.e message - echo the message on Telegram
68 |
69 | self.dp.add_handler(MessageHandler(Filters.command, self.custom_command))
70 | self.dp.add_handler(MessageHandler(Filters.text, self.user_input))
71 | self.dp.add_handler(MessageHandler(Filters.location, self.location))
72 | #
73 | # # log all errors
74 | self.dp.add_error_handler(self.error)
75 |
76 | self.scheduler = BackgroundScheduler()
77 | self.scheduler.start()
78 | self.scheduler.add_job(self.stats_checking, 'interval', minutes=1)
79 | # Start the Bot
80 | self.updater.start_polling(timeout=30)
81 | self.stats_checking()
82 |
83 | # Run the bot until you press Ctrl-C or the process receives SIGINT,
84 | # SIGTERM or SIGABRT. This should be used most of the time, since
85 | # start_polling() is non-blocking and will stop the bot gracefully.
86 | # updater.idle()
87 |
88 | def track(self, event: TgEvent, update, *params):
89 | user = update.message.from_user
90 | self.tracker.tg(event, user, *params)
91 |
92 |
93 | def broadcast_message(self, text):
94 | if not VRNBUSBOT_TOKEN:
95 | return
96 | now = datetime.datetime.now()
97 | if not (6 <= now.hour < 23):
98 | return
99 | for user_id in self.users_to_inform:
100 | try:
101 | self.bot.send_message(chat_id=user_id,
102 | text=text,
103 | parse_mode='Markdown')
104 | except Exception as e:
105 | self.logger.error(f'Error while sending message to {user_id=}, {e}')
106 |
107 | def stats_checking(self):
108 | def send_msg(text, force=False):
109 | if self.stats_fail_start:
110 | if self.stats_fail_start_last and (now - self.stats_fail_start_last) < datetime.timedelta(minutes=10):
111 | return
112 | self.stats_fail_start_last = now
113 |
114 | for user_id in self.users_to_inform:
115 | self.bot.send_message(chat_id=user_id,
116 | text=text,
117 | parse_mode='Markdown')
118 |
119 | now = datetime.datetime.now()
120 | if not (6 <= now.hour < 23):
121 | return
122 | response = self.cds.get_bus_statistics()
123 | if not response or response.min1 < 10 or response.min10 / response.min60 < 0.5:
124 | if not self.stats_fail_start:
125 | self.stats_fail_start = now
126 | send_msg(f'```\nПроверьте данные после {self.stats_fail_start:%H:%M:%S} \n{response and response.text}\n```')
127 | elif self.stats_fail_start:
128 | msg = f'```\nДанные снова актуальны после {self.stats_fail_start:%H:%M:%S} \n{response and response.text}\n```'
129 | self.stats_fail_start = None
130 | send_msg(msg)
131 |
132 | @run_async
133 | def custom_command(self, bot, update):
134 | command = update.message.text
135 | if command.startswith('/nextbus_'):
136 | match = re.match(r'/nextbus_(\d*)[ ]*(.*)', command)
137 | if match and match.group(1):
138 | id = int(match.group(1))
139 | bus_stop = self.cds.get_bus_stop_from_id(id)
140 | if bus_stop:
141 | self.track(TgEvent.CUSTOM_CMD, update, command)
142 | self.next_bus_for_bus_stop(update, bus_stop, match.group(2))
143 | return
144 |
145 | if command.startswith('/fb'):
146 | match = re.match(r'/fb[_]?(\S+)', command)
147 | if match and match.group(1):
148 | bus_name = match.group(1)
149 | self.track(TgEvent.CUSTOM_CMD, update, command)
150 | self.fb_link_show(bus_name, update)
151 | return
152 |
153 | self.track(TgEvent.WRONG_CMD, update, command, "Didn't find")
154 | bot.send_message(chat_id=update.message.chat_id,
155 | text=f"Sorry, I didn't understand that command. {update.message.text}")
156 |
157 | def error(self, _, update, error):
158 | """Log Errors caused by Updates."""
159 | self.logger.warning('Update "%s" caused error "%s"', update, error)
160 | if update:
161 | update.message.reply_text(f"Update caused error {error}")
162 |
163 | @run_async
164 | def start(self, _, update):
165 | self.track(TgEvent.START, update)
166 |
167 | location_keyboard = KeyboardButton(text="Местоположение", request_location=True)
168 | cancel_button = KeyboardButton(text="Отмена")
169 | custom_keyboard = [[location_keyboard, cancel_button]]
170 | reply_markup = ReplyKeyboardMarkup(custom_keyboard, one_time_keyboard=True)
171 | update.message.reply_text(
172 | "/nextbus имя остановки - ожидаемое время прибытия\n"
173 | "Отправка местоположения - ожидаемое время прибытия для ближайших "
174 | "трёх остановок\n"
175 | "/last номера маршрутов через пробел - последние "
176 | "остановки автобусов\n"
177 | "Свободный ввод - номера маршрутов и расстояние до автобусов "
178 | "(если отправляли местоположение)",
179 | reply_markup=reply_markup)
180 |
181 | @run_async
182 | def helpcmd(self, _, update):
183 | """Send a message when the command /help is issued."""
184 | user = update.message.from_user
185 | self.track(TgEvent.HELP, update)
186 | update.message.reply_text("""
187 | /nextbus имя остановки - ожидаемое время прибытия
188 |
189 | /stats - короткая статистика по автобусам онлайн
190 |
191 | /last номера маршрутов через пробел - последние остановки
192 |
193 | /settings [+|-|add|del|all|none|все] номера маршрутов - фильтрация по маршрутам
194 |
195 | Отправка местоположения - ожидаемое время прибытия для ближайших трёх остановок
196 | Свободный ввод - номера маршрутов и расстояние до автобусов (если отправляли местоположение)
197 |
198 | Примеры:
199 | /nextbus памятник славы - выведет прибытие на остановки:
200 | Памятник Славы (Московский проспект в центр),
201 | Памятник славы (Московский проспект из центра),
202 | Памятник славы (ул. Хользунова в центр)
203 |
204 | /last 5а 113кш - выведет последние остановки автобусов на маршрутах 5А и 113КШ
205 |
206 | /settings 27 5а - фильтрует автобусы в остальных командах, оставляя только выбранные
207 | /settings all - выбрать все (эквивалентно /settings none) или
208 | /settings все
209 |
210 | /settings add 104 125 - добавить к фильтру маршруты 104 125 или
211 | /settings + 104 125
212 |
213 | /settings del 37 52 - удалить из фильтра маршруты 37 52 или
214 | /settings - 37 52
215 | """,
216 | reply_markup=ReplyKeyboardRemove())
217 |
218 | def send_text(self, text, update, **kwargs):
219 | for part in textwrap.wrap(text, 4000, replace_whitespace=False):
220 | update.message.reply_text(part, **kwargs)
221 |
222 | @run_async
223 | def last_buses(self, _, update, args):
224 | """Send a message when the command /last is issued."""
225 | user = update.message.from_user
226 |
227 | self.track(TgEvent.LAST, update, args)
228 | user_loc = self.user_settings.get(user.id, {}).get('user_loc', None)
229 | route_params = parse_routes(args)
230 | if route_params.all_buses:
231 | update.message.reply_text('Укажите маршруты для вывода')
232 | return
233 |
234 | response = self.cds.bus_request(route_params, user_loc=user_loc)
235 | text = response[0]
236 | self.track(TgEvent.LAST, update, args)
237 | self.logger.debug(f"last_buses. User: {user}; Response {' '.join(text.split())}")
238 | self.send_text(text, update)
239 |
240 | def get_buttons_routes(self, user_routes):
241 | # TODO: too many buttons
242 | routes_list = sorted(list(self.cds.bus_routes.keys()), key=natural_sort_key)
243 | routes_groups = list(grouper(8, routes_list))
244 | route_btns = [[InlineKeyboardButton('Hide', callback_data='hide')],
245 | [InlineKeyboardButton('All', callback_data='all'),
246 | InlineKeyboardButton('None', callback_data='none')]
247 | ] + [
248 | [InlineKeyboardButton(f"{x}{'+' if x in user_routes else ''}", callback_data=x)
249 | for x in group if x]
250 | for group in routes_groups]
251 | keyboard = route_btns + [
252 | ]
253 | return keyboard
254 |
255 | @run_async
256 | def settings(self, _, update, args):
257 | user_id = update.message.from_user.id
258 | settings = self.user_settings.get(user_id, {})
259 | route_settings = settings.get('route_settings', [])
260 | input_routes = parse_routes(args)[1]
261 | if input_routes:
262 | cmd = input_routes[0].lower()
263 | change_routes = [y for x in input_routes
264 | for y in self.cds.bus_routes.keys() if x.upper() == y.upper()]
265 | if len(input_routes) == 1 and cmd in ('all', 'none', 'все'):
266 | route_settings = []
267 | elif cmd in ('del', '-'):
268 | route_settings = [x for x in route_settings if x not in change_routes]
269 | elif cmd in ('add', '+'):
270 | route_settings = list(set(route_settings + change_routes))
271 | else:
272 | route_settings = change_routes
273 | settings['route_settings'] = sorted(route_settings, key=natural_sort_key)
274 | self.user_settings[user_id] = settings
275 | update.message.reply_text(f"Текущие маршруты для вывода: {' '.join(route_settings)}")
276 | return
277 |
278 | keyboard = self.get_buttons_routes(route_settings)
279 | reply_markup = InlineKeyboardMarkup(keyboard)
280 |
281 | update.message.reply_text('Укажите маршруты для вывода:', reply_markup=reply_markup)
282 |
283 | @run_async
284 | def settings_button(self, bot, update):
285 | query = update.callback_query
286 | self.logger.info(query)
287 | user_id = query.message.chat_id
288 | user_settings = self.user_settings.get(user_id, {})
289 | settings = user_settings.get('route_settings', [])
290 | key = query.data
291 |
292 | if key == 'all':
293 | settings = list(self.cds.bus_routes.keys())
294 | elif key == 'none':
295 | settings = []
296 | elif key == 'hide':
297 | routes = ' '.join(settings) if settings else 'все доступные'
298 | bot.edit_message_text(text=f"Текущие маршруты для вывода: {routes}",
299 | chat_id=query.message.chat_id,
300 | message_id=query.message.message_id)
301 | return
302 | else:
303 | if key in settings:
304 | settings.remove(key)
305 | else:
306 | settings.append(key)
307 |
308 | user_settings['route_settings'] = settings
309 | self.user_settings[user_id] = user_settings
310 | keyboard = self.get_buttons_routes(settings)
311 | reply_markup = InlineKeyboardMarkup(keyboard)
312 | routes = ' '.join(settings) if settings else 'все доступные'
313 | bot.edit_message_text(text=f"Текущие маршруты для вывода: {routes}",
314 | chat_id=query.message.chat_id,
315 | message_id=query.message.message_id,
316 | reply_markup=reply_markup)
317 |
318 | def get_text_from_arrival_info(self, arrival_info: ArrivalInfo):
319 | def text_for_arrival_info(value):
320 | s = (f'```{value.text}```' if value else '')
321 | command = f'/nextbus_{value.bus_stop_id}'
322 | return f"[{command}]({command}) {value.bus_stop_name}\n{s} "
323 |
324 | def text_for_bus_stop(value: BusStop):
325 | command = f'/nextbus_{value.ID}'
326 | return f"[{command}]({command}) {value.NAME_}"
327 |
328 | if arrival_info.found:
329 | next_bus_text = '\n'.join([text_for_arrival_info(v) for v in arrival_info.arrival_details])
330 | else:
331 | next_bus_text = '\n'.join([text_for_bus_stop(v) for v in arrival_info.bus_stops])
332 | return f'{arrival_info.header}\n{next_bus_text}'
333 |
334 | @run_async
335 | def next_bus_general(self, update, args):
336 | user = update.message.from_user
337 |
338 | self.track(TgEvent.NEXT, update, args)
339 | if not args:
340 | location_btn = KeyboardButton(text="Местоположение", request_location=True)
341 | cancel_btn = KeyboardButton(text="Отмена")
342 | custom_keyboard = [[location_btn, cancel_btn]]
343 | reply_markup = ReplyKeyboardMarkup(custom_keyboard, one_time_keyboard=True)
344 | update.message.reply_text("""Не указана остановка, попробуйте указать местоположение""",
345 | reply_markup=reply_markup)
346 | return
347 |
348 | user_settings = self.user_settings.get(user.id, {})
349 | search_result = SearchResult(bus_routes=tuple(user_settings.get('route_settings', [])))
350 | bus_stop_name = args if isinstance(args, str) else ' '.join(args)
351 | response = self.cds.next_bus(bus_stop_name, search_result)
352 | update.message.reply_text(self.get_text_from_arrival_info(response), parse_mode='Markdown')
353 |
354 | def next_bus_for_bus_stop(self, update, bus_stop, params):
355 | user = update.message.from_user
356 |
357 | search_params = parse_routes(params)
358 |
359 | self.track(TgEvent.NEXT, update, bus_stop, params)
360 |
361 | response = self.cds.next_bus_for_matches((bus_stop,), search_params)
362 | update.message.reply_text(self.get_text_from_arrival_info(response), parse_mode='Markdown')
363 |
364 | def next_bus_handler(self, _, update, args):
365 | self.next_bus_general(update, args)
366 |
367 | def fb_link_handler(self, _, update, args):
368 | bus_name = args if isinstance(args, str) else ' '.join(args)
369 | self.fb_link_show(bus_name, update)
370 |
371 | def fb_link_show(self, bus_name, update):
372 | fotobus_links = fb_links(bus_name)
373 | command = f'/fb_{bus_name}'
374 | update.message.reply_text("\n".join((f"[{command}]({command}) [{link}]({link})" for link in fotobus_links)), parse_mode='Markdown')
375 |
376 | @run_async
377 | def send_stats(self, update, full_info):
378 | user = update.message.from_user
379 |
380 | self.track(TgEvent.STATS, update, full_info)
381 | response = self.cds.get_bus_statistics(full_info) or StatsData(0, 0, 0, 0, 0, "Нет данных")
382 | update.message.reply_text(f'/stats ```\n{response.text}\n```', parse_mode='Markdown')
383 |
384 | def stats(self, _, update):
385 | self.send_stats(update, False)
386 |
387 | def stats_full(self, _, update):
388 | self.send_stats(update, True)
389 |
390 | @run_async
391 | def user_stats(self, _, update):
392 | self.track(TgEvent.USER_STATS, update)
393 | stats = self.tracker.stats()
394 | self.logger.debug(stats)
395 | update.message.reply_text(f'```\n{stats}\n```',
396 | parse_mode='Markdown')
397 |
398 | @run_async
399 | def user_stats_pro(self, bot, update, args):
400 | if update.message.from_user.id not in self.users_to_inform:
401 | self.logger.error(f"Unknown user {update.message.from_user}")
402 | return
403 | self.track(TgEvent.USER_STATS, update)
404 | threshold, valid_threshold = parse_int(args[:1], 50)
405 | user_filter = ''.join(args if not valid_threshold else args[1:])
406 | event_filter = [get_event_by_name(i) for i in args if get_event_by_name(i)]
407 | stats = self.tracker.stats(True, threshold, user_filter, event_filter)
408 | self.send_text(f'```\n{stats}\n```', update,
409 | parse_mode='Markdown')
410 |
411 | @run_async
412 | def user_input(self, bot, update):
413 | message = update.message
414 | user = message.from_user
415 | text = message.text
416 | l_text = text.lower()
417 |
418 | self.track(TgEvent.USER_INPUT, update, text[:30])
419 | if not text or text == 'Отмена':
420 | message.reply_text(text=f"Попробуйте воспользоваться справкой /help",
421 | reply_markup=ReplyKeyboardRemove())
422 | return
423 |
424 | if l_text == 'на рефакторинг!':
425 | message.reply_text('Тогда срочно сюда @deeprefactoring!')
426 | return
427 |
428 | if self.cds.is_bus_stop_name(text):
429 | self.next_bus_general(update, text.split(' '))
430 | return
431 |
432 | if l_text.startswith("ост") or l_text.startswith("аст"):
433 | args = text.lower().split(' ')[1:]
434 | self.next_bus_general(update, args)
435 | return
436 |
437 | match = re.search('https://maps\.google\.com/maps\?.*&ll=(?P[-?\d.]*),(?P[-?\d.]*)', text)
438 | if match:
439 | (lat, lon) = (match.group('lat'), match.group('lon'))
440 | self.show_arrival(update, float(lat), float(lon))
441 | else:
442 | user_loc = self.user_settings.get(user.id, {}).get('user_loc', None)
443 | self.logger.info(f"User: {user} '{text}' {user_loc}")
444 | route_params = parse_routes(text)
445 | if route_params.all_buses:
446 | update.message.reply_text('Укажите маршруты для вывода')
447 | return
448 | response = self.cds.bus_request(route_params, user_loc=user_loc)
449 | self.logger.debug(f'"{text}" User: {user}; Response: {response[:5]} from {len(response)}')
450 | reply_text = response[0]
451 | for part in textwrap.wrap(reply_text, 4000, replace_whitespace=False):
452 | update.message.reply_text(part, reply_markup=ReplyKeyboardRemove())
453 |
454 | def show_arrival(self, update, lat, lon):
455 | user = update.message.from_user
456 |
457 | self.track(TgEvent.NEXT, update, lat, lon)
458 | self.logger.info(f"User: {user} {lat}, {lon}")
459 | matches = self.cds.matches_bus_stops(lat, lon)
460 | user_loc = UserLoc(lat, lon)
461 | settings = self.user_settings.get(user.id, {})
462 | settings['user_loc'] = user_loc
463 | self.user_settings[user.id] = settings
464 | bus_routes = settings.get('route_settings')
465 | search_result = SearchResult(bus_routes=(bus_routes if bus_routes else tuple()))
466 | arrival_info = self.cds.next_bus_for_matches(tuple(matches), search_result)
467 | self.logger.debug(f"next_bus_for_matches {user} {arrival_info}")
468 | update.message.reply_text(self.get_text_from_arrival_info(arrival_info), parse_mode='Markdown',
469 | reply_markup=ReplyKeyboardRemove())
470 |
471 | @run_async
472 | def location(self, _, update):
473 | loc = update.message.location
474 | (lat, lon) = loc.latitude, loc.longitude
475 | self.show_arrival(update, lat, lon)
476 |
--------------------------------------------------------------------------------
/fe/map.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | "use strict";
3 |
4 | if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
5 | location.href = 'https:' + window.location.href.substring(window.location.protocol.length);
6 | }
7 |
8 | var coords = {latitude: 51.6754966, longitude: 39.2088823}
9 |
10 | var lastbusquery = document.getElementById('lastbusquery')
11 | var station_query = document.getElementById('station_query')
12 | var station_name = document.getElementById('station_name')
13 | var my_map
14 | var BusIconContentLayout
15 | var timer_id = 0
16 |
17 | var info = document.getElementById('info')
18 | var businfo = document.getElementById('businfo')
19 | var lastbus_map_update = document.getElementById('lastbus_map_update')
20 | var nextbus_loading = document.getElementById('nextbus_loading')
21 | var lastbus_loading = document.getElementById('lastbus_loading')
22 | var cb_refresh = document.getElementById('cb_refresh')
23 | var cb_show_info = document.getElementById('cb_show_info')
24 | var btn_station_search = document.getElementById('btn_station_search')
25 |
26 | var bus_stop_list = []
27 | var bus_stop_names = []
28 | var bus_stop_auto_complete
29 |
30 | if (lastbus_map_update)
31 | lastbus_map_update.onclick = function () {
32 | update_bus_map()
33 | }
34 |
35 | if (cb_refresh)
36 | cb_refresh.onclick = function () {
37 | if (!cb_refresh.checked) {
38 | clearTimeout(timer_id)
39 | clearTimeout(timer_stop_id)
40 | timer_id = 0
41 | timer_stop_id = 0
42 | }
43 | }
44 |
45 | if (cb_show_info) {
46 | cb_show_info.onclick = function () {
47 | var show = cb_show_info.checked
48 | businfo.className = show ? "" : "hide_info"
49 | }
50 | }
51 |
52 | if (btn_station_search) {
53 | btn_station_search.onclick = function () {
54 | run_search_by_name()
55 | }
56 | }
57 | if (lastbusquery) {
58 | lastbusquery.onkeyup = function (event) {
59 | event.preventDefault()
60 | if (event.keyCode === 13) {
61 | update_bus_map()
62 | }
63 | }
64 | }
65 |
66 | if (station_query) {
67 | station_query.onkeyup = function (event) {
68 | event.preventDefault()
69 | if (event.keyCode === 13) {
70 | run_search_by_name()
71 | }
72 | }
73 | }
74 |
75 | if (station_name) {
76 | station_name.onkeyup = function (event) {
77 | event.preventDefault()
78 | if (event.keyCode === 13) {
79 | run_search_by_name()
80 | }
81 | }
82 | }
83 |
84 | function set_query_parameter(name, value) {
85 | const params = new URLSearchParams(window.location.search);
86 | params.set(name, value);
87 | window.history.replaceState({}, "", decodeURIComponent(`${window.location.pathname}?${params}`));
88 | }
89 |
90 | function run_timer(func) {
91 | if (cb_refresh.checked && !timer_id) {
92 | timer_id = setTimeout(function tick() {
93 | func().then(function () {
94 | if (cb_refresh.checked)
95 | timer_id = setTimeout(tick, 30 * 1000)
96 | })
97 | }, 30 * 1000)
98 | }
99 | }
100 |
101 | function run_search_by_name() {
102 | run_timer(run_search_by_name)
103 |
104 | update_user_position()
105 | return get_bus_arrival_by_name()
106 | }
107 |
108 | function update_bus_map() {
109 | run_timer(update_bus_map)
110 |
111 | var bus_query = lastbusquery.value
112 | save_to_ls('bus_query', bus_query)
113 | update_user_position()
114 | return get_bus_positions(bus_query)
115 | }
116 |
117 | if ("geolocation" in navigator) {
118 | var nextbus = document.getElementById('nextbus')
119 |
120 | if (nextbus)
121 | nextbus.onclick = function (event) {
122 | event.preventDefault()
123 | get_current_pos(get_bus_arrival)
124 | }
125 | }
126 |
127 | function update_user_position() {
128 | if ("geolocation" in navigator) {
129 | navigator.geolocation.getCurrentPosition(function (position) {
130 | coords = position.coords
131 | })
132 | }
133 | }
134 |
135 | function save_station_params() {
136 | var query = station_query.value
137 | var station = station_name.value
138 |
139 | save_to_ls('station_query', query)
140 | save_to_ls('station', station)
141 | }
142 |
143 | function get_current_pos(func) {
144 | save_station_params()
145 |
146 | update_user_position()
147 | }
148 |
149 | function format_bus_stops(header, bus_stops) {
150 | var bus_stop_info = header + '\n'
151 | for (var prop in bus_stops) {
152 | bus_stop_info += '' + prop + '' + '\n' + bus_stops[prop] + '\n'
153 | }
154 |
155 | info.innerHTML = bus_stop_info
156 | var elements = document.getElementsByClassName('bus_linked')
157 | for (var i = 0; i < elements.length; i++) {
158 | elements[i].onclick = function (e) {
159 | e.preventDefault()
160 | if (e.srcElement && e.srcElement.text) {
161 | station_name.value = e.srcElement.text
162 | get_bus_arrival_by_name()
163 | }
164 | }
165 | }
166 | }
167 |
168 | function get_bus_arrival_by_name() {
169 | var btn_station_search = document.getElementById('btn_station_search')
170 | waiting(nextbus_loading, btn_station_search, true)
171 |
172 | var bus_query = station_query.value
173 | var station = station_name.value
174 |
175 | save_station_params()
176 |
177 | var params = 'q=' + encodeURIComponent(bus_query) +
178 | '&station=' + encodeURIComponent(station)
179 |
180 | return fetch('/bus_stop_search?' + params,
181 | {
182 | method: 'GET',
183 | headers: {
184 | 'Content-Type': 'application/json'
185 | },
186 | credentials: 'include',
187 | })
188 | .then(function (res) {
189 | return res.json()
190 | })
191 | .then(function (data) {
192 | update_cookies()
193 | waiting(nextbus_loading, btn_station_search, false)
194 | format_bus_stops(data.header, data.bus_stops)
195 | })
196 | .catch(function (error) {
197 | waiting(nextbus_loading, btn_station_search, false)
198 | info.innerHTML = 'Ошибка: ' + error
199 | })
200 | }
201 |
202 | function get_bus_arrival(position) {
203 | var nextbus = document.getElementById('nextbus')
204 | waiting(nextbus_loading, nextbus, true)
205 |
206 | coords = position.coords
207 | var bus_query = station_query.value
208 |
209 | var params = 'q=' + encodeURIComponent(bus_query) +
210 | '&lat=' + encodeURIComponent(coords.latitude) +
211 | '&lon=' + encodeURIComponent(coords.longitude)
212 |
213 | return fetch('/arrival?' + params,
214 | {
215 | method: 'GET',
216 | headers: {
217 | 'Content-Type': 'application/json'
218 | },
219 | credentials: 'include',
220 | })
221 | .then(function (res) {
222 | return res.json()
223 | })
224 | .then(function (data) {
225 | update_cookies()
226 | waiting(nextbus_loading, nextbus, false)
227 | format_bus_stops(data.header, data.bus_stops)
228 | })
229 | .catch(function (error) {
230 | waiting(nextbus_loading, nextbus, false)
231 | info.innerHTML = 'Ошибка: ' + error
232 | })
233 | }
234 |
235 |
236 | function waiting(element, button, state) {
237 | element.className = state ? 'spinner' : ''
238 | button.disabled = state
239 | }
240 |
241 | function fraud_check() {
242 | if (parent !== window) {
243 | return "&parentUrl=" + encodeURIComponent(document.referrer)
244 | }
245 | return ""
246 | }
247 |
248 | function diff_time(last_time, max_time) {
249 | var date_1 = new Date(last_time)
250 | var date_2 = new Date(max_time)
251 |
252 | return (date_2 - date_1)/1000;
253 | }
254 |
255 | function formate_date(last_time) {
256 | function pad_zero(number) {
257 | return ('0' + number).slice(-2)
258 | }
259 |
260 | var date = new Date(last_time)
261 | var time = last_time.substring(last_time.length - 8)
262 | if (Date.now() - date > (3600 * 1000 * 24)) {
263 | time = date.getFullYear() + '-' + pad_zero(date.getMonth() + 1) + '-' + pad_zero(date.getDate()) + ' ' + time
264 | }
265 |
266 | return time
267 | }
268 |
269 | function get_bus_positions(query) {
270 | waiting(lastbus_loading, lastbus_map_update, true)
271 | set_query_parameter('bus_query', query)
272 | var params = 'src=map&q=' + encodeURIComponent(query) + fraud_check()
273 | if (coords) {
274 | params += '&lat=' + encodeURIComponent(coords.latitude)
275 | params += '&lon=' + encodeURIComponent(coords.longitude)
276 | }
277 |
278 | return fetch('/busmap?' + params,
279 | {
280 | method: 'GET',
281 | headers: {
282 | 'Content-Type': 'application/json'
283 | },
284 | credentials: 'include',
285 | })
286 | .then(function (res) {
287 | return res.json()
288 | })
289 | .then(function (data) {
290 | update_cookies()
291 | waiting(lastbus_loading, lastbus_map_update, false)
292 | var q = data.q
293 | var text = data.text
294 | var server_time = new Date(data.server_time)
295 | businfo.innerHTML = 'Маршруты: ' + q + '\nКоличество результатов: ' + data.buses.length + '\n' + text
296 |
297 | if (!my_map)
298 | return
299 |
300 | if (data.buses.length > 0) {
301 | var min_last_time = data.buses[0][0].last_time_;
302 | var max_time = data.buses.reduce(function (accumulator, currentValue) {
303 | if (currentValue[0].last_time_ > accumulator)
304 | return currentValue[0].last_time_;
305 | return accumulator;
306 | }, min_last_time);
307 |
308 | var delta_with_current = (new Date() - new Date(max_time))/1000
309 | var delta_with_max = (server_time - new Date(max_time))/1000
310 | //
311 | // console.log("current_time - max_time", delta_with_current.toFixed(1))
312 | // console.log("server_time - max_time", delta_with_max.toFixed(1))
313 | }
314 |
315 | var bus_with_azimuth = data.buses.map(function (data) {
316 | var bus = data[0]
317 | var next_bus_stop = data[1]
318 | if (!next_bus_stop.LON_ || !next_bus_stop.LAT_) {
319 | return bus
320 | }
321 |
322 | bus.hint = next_bus_stop.NAME_
323 |
324 | var x = next_bus_stop.LAT_ - bus.last_lat_
325 | var y = next_bus_stop.LON_ - bus.last_lon_
326 |
327 | bus.db_azimuth = bus.azimuth
328 | bus.azimuth = Math.floor(Math.atan2(y, x) * 180 / Math.PI)
329 | var time = formate_date(bus.last_time_)
330 | var bus_type = "МВ"
331 | switch (bus.bus_type) {
332 | case 3:
333 | bus_type = "СВ"
334 | break
335 | case 4:
336 | bus_type = "БВ"
337 | break
338 | }
339 | var bus_output = bus.obj_output === 1 ? ' ! ' : ''
340 |
341 | bus.delta_time = diff_time(bus.last_time_, server_time);
342 | var show_bus_name = bus.name_ && !bus.hidden_name
343 | var bus_name = show_bus_name ? bus.name_ : bus.hidden_name;
344 | var route_name = bus.route_name_.trim();
345 | var fb_link_info = " " + route_name + " " + bus.name_ + "";
346 |
347 | bus.desc = [bus_output + time + " " + next_bus_stop.NAME_,
348 | (show_bus_name ? fb_link_info : route_name) + " " + bus.bort_name,
349 | bus.last_speed_.toFixed(1)
350 | + " ~ " + bus.avg_speed.toFixed(1)
351 | + " ~ " + bus.avg_last_speed.toFixed(1) + ' км/ч',
352 | "Азимуты: к остановке " + bus.azimuth + '; ' + bus.db_azimuth,
353 | (bus.low_floor ? "Низкопол" : "") + " " + bus_type,
354 | 'Отправить обращение',
355 |
356 | ].join('
')
357 |
358 |
359 | return bus
360 | })
361 | update_map(bus_with_azimuth, true)
362 | }).catch(function (error) {
363 | waiting(lastbus_loading, lastbus_map_update, false)
364 | console.error(error)
365 | businfo.innerHTML = 'Ошибка: ' + error
366 | })
367 | }
368 |
369 | function get_bus_stop_list() {
370 | return fetch('/bus_stops.json',
371 | {
372 | method: 'GET',
373 | headers: {
374 | 'Content-Type': 'application/json'
375 | },
376 | credentials: 'include',
377 | })
378 | .then(function (res) {
379 | return res.json()
380 | })
381 | .then(function (data) {
382 | update_cookies()
383 | bus_stop_list = data
384 | bus_stop_names = bus_stop_list.map(function callback(bus_stop) {
385 | return bus_stop.NAME_
386 | })
387 | if (station_name) {
388 | bus_stop_auto_complete = new autoComplete({
389 | selector: station_name,
390 | source: function (term, suggest) {
391 | term = term.toLowerCase();
392 | var matches = [];
393 | for (var i = 0; i < bus_stop_names.length; i++)
394 | if (~bus_stop_names[i].toLowerCase().indexOf(term)) matches.push(bus_stop_names[i]);
395 | suggest(matches);
396 | }
397 | })
398 | }
399 |
400 | })
401 | }
402 |
403 |
404 | function get_bus_list() {
405 | return fetch('/buslist',
406 | {
407 | method: 'GET',
408 | headers: {
409 | 'Content-Type': 'application/json'
410 | },
411 | credentials: 'include',
412 | })
413 | .then(function (res) {
414 | return res.json()
415 | })
416 | .then(function (data) {
417 | update_cookies()
418 | var bus_list = data.result
419 |
420 | var select = document.getElementById('buslist')
421 | select.appendChild(new Option('Маршруты', '-'))
422 | bus_list.forEach(function (bus_name) {
423 | var opt = new Option(bus_name, bus_name)
424 | select.appendChild(opt)
425 | })
426 |
427 | select.onchange = function () {
428 | var text = select.options[select.selectedIndex].value; // Текстовое значение для выбранного option
429 | if (text !== '-') {
430 | if (lastbusquery)
431 | lastbusquery.value += ' ' + text
432 | if (station_query)
433 | station_query.value += ' ' + text
434 | }
435 | }
436 | })
437 | }
438 |
439 | function update_map(buses, clear) {
440 | if (!my_map) {
441 | return
442 | }
443 |
444 | var objectManager = new ymaps.ObjectManager()
445 |
446 | objectManager.objects.options.set({
447 | iconLayout: 'default#imageWithContent',
448 | iconImageHref: 'bus_round_copy.png',
449 | iconImageSize: [32, 32],
450 | iconImageOffset: [-16, -16],
451 | iconContentOffset: [0, 0],
452 | iconContentLayout: BusIconContentLayout,
453 | balloonMaxWidth: 250,
454 | })
455 |
456 | var features = []
457 |
458 |
459 | if (clear) {
460 | my_map.geoObjects.removeAll()
461 | }
462 |
463 |
464 | buses.forEach(function (bus, index) {
465 | features.push(add_bus(bus, index))
466 | })
467 |
468 | objectManager.add({
469 | "type": "FeatureCollection",
470 | "features": features
471 | })
472 |
473 | my_map.geoObjects.add(objectManager)
474 | }
475 |
476 | function add_stop(stop, id) {
477 | if (!stop) {
478 | return
479 | }
480 | var hint_content = stop.NAME_
481 | var balloon_content = stop.NAME_
482 | var lat = stop.LAT_
483 | var lon = stop.LON_
484 |
485 | return {
486 | "type": "Feature",
487 | "id": id,
488 | "geometry": {"type": "Point", "coordinates": [lat, lon]},
489 | "properties": {
490 | "balloonContent": balloon_content,
491 | "hintContent": hint_content,
492 | "clusterCaption": hint_content
493 | }
494 | }
495 | }
496 |
497 |
498 | function add_bus(bus, id, max_time) {
499 | if (!bus) {
500 | return
501 | }
502 | var hint_content = bus.hint ? bus.hint : bus.last_time_ + '; ' + bus.azimuth
503 | var balloon_content = bus.desc ? bus.desc : bus.last_time_ + JSON.stringify(bus, null, ' ')
504 | var lat = bus.lat2 || bus.last_lat_
505 | var lon = bus.lon2 || bus.last_lon_
506 | var bus_output = bus.obj_output === 1 ? ' !' : ''
507 | var show_bus_name = bus.name_ && !bus.hidden_name
508 |
509 | var icon_content = bus_output + " " + bus.route_name_.trim() + (show_bus_name ? " " + bus.name_ : "")
510 | var rotation = bus.db_azimuth
511 | var wait = bus.delta_time < 60 ? '' : '_wait';
512 | if (bus.delta_time > 180){
513 | wait = '_long_wait'
514 | }
515 | var file_name = bus.low_floor === 1 ? 'bus_round_lf' : 'bus_round';
516 |
517 | return {
518 | "type": "Feature",
519 | "id": id,
520 | "geometry": {"type": "Point", "coordinates": [lat, lon]},
521 | "properties": {
522 | "balloonContent": balloon_content,
523 | "hintContent": hint_content,
524 | "iconContent": icon_content,
525 | "rotation": rotation,
526 | "clusterCaption": icon_content + ' ' + hint_content,
527 | 'iconImageHref': file_name + wait +'.png',
528 | },
529 | "options": {
530 | iconImageHref: "img/" + file_name + wait +'.png',
531 | }
532 | }
533 | }
534 |
535 | if ('ymaps' in window) {
536 | ymaps.ready(ymap_show);
537 | }
538 |
539 | function add_bus_stops(stops) {
540 | var objectManager = new ymaps.ObjectManager({
541 | clusterize: true,
542 | gridSize: 80,
543 | clusterDisableClickZoom: true
544 | })
545 |
546 | objectManager.objects.options.set({
547 | preset: 'islands#darkGreenCircleDotIcon'
548 | })
549 |
550 | var features = []
551 |
552 | stops.forEach(function (stop, index) {
553 | features.push(add_stop(stop, index))
554 | })
555 |
556 | objectManager.add({
557 | "type": "FeatureCollection",
558 | "features": features
559 | })
560 |
561 | my_map.events.add('click', function (e) {
562 | my_map.balloon.open(e.get('coords'), 'Щелк!');
563 | e.stopPropagation()
564 | e.preventDefault();
565 | });
566 | my_map.geoObjects.add(objectManager)
567 | }
568 |
569 | function ymap_show() {
570 | var map_zoom = load_from_ls('map_zoom') || 13
571 | var map_lat = load_from_ls('map_lat') || coords.latitude
572 | var map_lon = load_from_ls('map_lon') || coords.longitude
573 |
574 | my_map = new ymaps.Map('map', {
575 | center: [map_lat, map_lon],
576 | zoom: map_zoom
577 | }, {
578 | searchControlProvider: 'yandex#search',
579 | minZoom: 10,
580 | maxZoom: 19
581 | })
582 |
583 | my_map.events.add('boundschange', function (event) {
584 | save_to_ls('map_zoom', event.get('newZoom'))
585 | var center = event.get('newCenter')
586 |
587 | save_to_ls('map_lat', center[0])
588 | save_to_ls('map_lon', center[1])
589 | });
590 |
591 | BusIconContentLayout = ymaps.templateLayoutFactory.createClass(
592 | '
' +
593 | ' $[properties.iconContent] '
594 | )
595 |
596 | // TODO: Check with buses
597 | // add_bus_stops(bus_stop_list)
598 | if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
599 | }
600 | update_bus_map()
601 |
602 | }
603 |
604 | function save_to_ls(key, value) {
605 | if (!ls_test()) {
606 | return
607 | }
608 | localStorage.setItem(key, value)
609 | }
610 |
611 | function load_from_ls(key) {
612 | if (!ls_test()) {
613 | return
614 | }
615 | return localStorage.getItem(key)
616 | }
617 |
618 | function ls_test() {
619 | var test = 'test'
620 | if (!'localStorage' in window) {
621 | return false
622 | }
623 | try {
624 | localStorage.setItem(test, test)
625 | localStorage.removeItem(test)
626 | return true
627 | } catch (e) {
628 | return false
629 | }
630 | }
631 |
632 | function setCookie(name, value, options) {
633 | options = options || {};
634 |
635 | var expires = options.expires;
636 |
637 | if (typeof expires == "number" && expires) {
638 | var d = new Date();
639 | d.setTime(d.getTime() + expires * 1000);
640 | expires = options.expires = d;
641 | }
642 | if (expires && expires.toUTCString) {
643 | options.expires = expires.toUTCString();
644 | }
645 |
646 | value = encodeURIComponent(value);
647 |
648 | var updatedCookie = name + "=" + value;
649 |
650 | for (var propName in options) {
651 | updatedCookie += "; " + propName;
652 | var propValue = options[propName];
653 | if (propValue !== true) {
654 | updatedCookie += "=" + propValue;
655 | }
656 | }
657 |
658 | document.cookie = updatedCookie;
659 | }
660 |
661 | function getCookie(name) {
662 | var matches = document.cookie.match(new RegExp(
663 | "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
664 | ));
665 | return matches ? decodeURIComponent(matches[1]) : undefined;
666 | }
667 |
668 |
669 | function update_cookies() {
670 | var user_ip = getCookie("user_ip")
671 | if (user_ip) {
672 | save_to_ls("user_ip", user_ip)
673 | }
674 |
675 | var ls_user_ip = load_from_ls('user_ip')
676 | if (!user_ip && ls_user_ip) {
677 | setCookie("user_ip", ls_user_ip, {expires: 3600 * 24 * 7})
678 | }
679 | }
680 |
681 |
682 | function init() {
683 | update_cookies()
684 | if (station_name) {
685 | get_bus_stop_list()
686 | }
687 | get_bus_list()
688 |
689 | const params = new URLSearchParams(window.location.search);
690 | const bus_query = params.get('bus_query')
691 |
692 | if (lastbusquery) {
693 | lastbusquery.value = params.get('bus_query') || load_from_ls('bus_query') || ''
694 | }
695 |
696 | if (station_query)
697 | station_query.value = load_from_ls('station_query') || ''
698 |
699 | if (station_name)
700 | station_name.value = load_from_ls('station') || ''
701 | }
702 |
703 | document.addEventListener("DOMContentLoaded", init);
704 | })()
--------------------------------------------------------------------------------