Skip to content

Commit

Permalink
Merge pull request #49 from bycs/dev
Browse files Browse the repository at this point in the history
Added the ability to search for the nearest bar.
  • Loading branch information
bycs authored Sep 19, 2022
2 parents be62f95 + 805fd5e commit 2a3ce02
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 9 deletions.
4 changes: 3 additions & 1 deletion .env_template
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ DJANGO_SECRET_KEY=django_secret_key
DJANGO_ALLOWED_HOSTS=localhost 0.0.0.0 127.0.0.1 [::1]
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost http://0.0.0.0 http://127.0.0.1

TELEGRAM_BOT_TOKEN=telegram_bot_token
TELEGRAM_BOT_TOKEN=token
GRAPHHOPPER_KEY=token
YANDEXMAP_KEY=token

DB_HOST=host
DB_USER=user
Expand Down
99 changes: 99 additions & 0 deletions beers/logics/geo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from dataclasses import dataclass
from math import atan2
from math import cos
from math import radians
from math import sin
from math import sqrt

import requests

from config import GRAPHHOPPER_KEY
from config import YANDEXMAP_KEY


@dataclass
class Point:
latitude: float
longitude: float

def __init__(self, latitude: float, longitude: float) -> None:
self.latitude = float(latitude)
self.longitude = float(longitude)

def __str__(self):
return f"{self.latitude}, {self.longitude}"

def to_dict(self):
point: dict = {"latitude": self.latitude, "lng": self.longitude}
return point


def get_distance(point1: Point, point2: Point) -> float:
R = 6372800 # Earth radius in meters
latitude_1 = radians(point1.latitude)
latitude_2 = radians(point2.latitude)
longitude_1 = radians(point1.longitude)
longitude_2 = radians(point2.longitude)
dlat = latitude_2 - latitude_1
dlon = longitude_2 - longitude_1
a = sin(dlat / 2) ** 2 + cos(latitude_1) * cos(latitude_2) * sin(dlon / 2) ** 2
c = 2 * atan2(sqrt(a), sqrt(1 - a))
distance = R * c
return distance


class YandexMapGeo:
KEY = YANDEXMAP_KEY

def geocode(self, address: str) -> Point | None:
url = "https://geocode-maps.yandex.ru/1.x/"
params: dict = {
"geocode": address,
"format": "json",
"results": 1,
"apikey": self.KEY,
"lang": "ru_RU",
}
r = requests.get(url, params=params)
response = r.json()
try:
featuremember = response["response"]["GeoObjectCollection"]["featureMember"][0]
coordinates = featuremember["GeoObject"]["Point"]["pos"].split()
latitude, longitude = coordinates
point = Point(latitude, longitude)
return point
except KeyError or IndexError:
return None


class GraphhopperGeo:
KEY = GRAPHHOPPER_KEY

def geocode(self, address: str) -> Point | None:
url = "https://graphhopper.com/api/1/geocode"
params: dict = {"q": address, "locale": "ru", "limit": 1, "key": self.KEY}
r = requests.get(url, params=params)
response = r.json()
try:
latitude = response["hits"][0]["point"]["lat"]
longitude = response["hits"][0]["point"]["lng"]
point = Point(latitude, longitude)
return point
except KeyError or IndexError:
return None

def route(self, from_p: Point, to_p: Point) -> str | None:
url = "https://graphhopper.com/api/1/route"
params = {
"point": [f"{from_p.latitude},{from_p.longitude}", f"{to_p.latitude},{to_p.longitude}"],
"vehicle": "foot",
"locale": "ru",
"key": self.KEY,
}
r = requests.get(url, params=params)
response = r.json()
try:
distance: str = response["paths"][0]["distance"]
return distance
except KeyError or IndexError:
return None
23 changes: 23 additions & 0 deletions beers/migrations/0005_alter_beer_managers_barbranch_longitude.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.1.1 on 2022-09-19 09:02

from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("beers", "0004_bar_updated"),
]

operations = [
migrations.AlterModelManagers(
name="beer",
managers=[],
),
migrations.AddField(
model_name="barbranch",
name="longitude",
field=models.FloatField(default=None, null=True),
),
]
19 changes: 19 additions & 0 deletions beers/migrations/0006_barbranch_latitude.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.1.1 on 2022-09-19 09:18

from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("beers", "0005_alter_beer_managers_barbranch_longitude"),
]

operations = [
migrations.AddField(
model_name="barbranch",
name="latitude",
field=models.FloatField(default=None, null=True),
),
]
19 changes: 19 additions & 0 deletions beers/models/bars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.db import models

from beers.logics.geo import YandexMapGeo


class Bar(models.Model):
name = models.CharField(max_length=255, unique=True)
Expand All @@ -17,13 +19,30 @@ class BarBranch(models.Model):
bar = models.ForeignKey(Bar, on_delete=models.CASCADE)
address = models.CharField(max_length=255, unique=True)
metro = models.CharField(max_length=255)
latitude = models.FloatField(null=True, default=None)
longitude = models.FloatField(null=True, default=None)

@property
def bar_branch_name(self):
return f"{self.bar.name} - {self.metro}"

@property
def point(self):
if (self.latitude is None or self.longitude is None) and self.address is not None:
self.get_geocode()
return f"{self.latitude},{self.longitude}"

def __str__(self):
return f"{self.bar.name} - {self.metro}"

def __repr__(self):
return f"<BarBranch: {self.bar.name} - {self.metro}>"

def get_geocode(self) -> None:
geo = YandexMapGeo()
point = geo.geocode(address=self.address)
if point is not None:
self.latitude = point.latitude
self.longitude = point.longitude
self.save()
print(f"Geocode for {self.address} is {point}")
6 changes: 4 additions & 2 deletions bot/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from aiogram import executor
from aiogram.contrib.fsm_storage.memory import MemoryStorage

from bot.logics.bars import BarBranchHandler
from bot.logics.bars import BarBranchGeo
from bot.logics.bars import BarBranchList
from bot.logics.bars import BarHandler
from bot.logics.base import Basic
from bot.logics.beers import FilterBeers
Expand All @@ -24,7 +25,8 @@

def run_bot() -> None:
print("### The bot is being launched")
BarBranchHandler(dp)
BarBranchList(dp)
BarBranchGeo(dp)
BarHandler(dp)
Basic(dp)
FilterBeers(dp)
Expand Down
5 changes: 5 additions & 0 deletions bot/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ class FilterForm(StatesGroup):
bar = State()
search_terms = State()
request = State()


class GeoBarForm(StatesGroup):
bar = State()
search_type = State()
111 changes: 108 additions & 3 deletions bot/logics/bars.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from collections import Counter
from operator import itemgetter

from aiogram import Dispatcher
from aiogram import md
from aiogram import types
from aiogram.dispatcher import FSMContext

from beers.logics.geo import Point
from beers.logics.geo import YandexMapGeo
from beers.logics.geo import get_distance
from beers.logics.utils import get_bars
from beers.logics.utils import get_bars_branches
from bot.forms import AddressForm
from bot.forms import GeoBarForm
from bot.logics.base import BaseLogic


Expand All @@ -22,7 +30,7 @@ async def command_bars(message: types.Message) -> None:
await message.reply(text)


class BarBranchHandler(BaseLogic):
class BarBranchList(BaseLogic):
def _register(self, dp: Dispatcher) -> None:
dp.register_message_handler(self.addresses_start, commands="addresses")
dp.register_message_handler(self.addresses_step2, state=AddressForm.bar)
Expand Down Expand Up @@ -51,6 +59,7 @@ async def addresses_step2(message: types.Message, state: FSMContext) -> None:
bars_branch = get_bars_branches(data["bar"])
data["bars_branch"] = bars_branch
metro_list = list(bars_branch.values_list("metro", flat=True))
metro_list = list(set(metro_list))
metro_list.sort()
markup.add(*metro_list)
markup.add("Показать все")
Expand All @@ -74,9 +83,105 @@ async def addresses_finish(message: types.Message, state: FSMContext) -> None:
else:
text = ""
for bar in bars_branch_list:
text += f"🍻 {bar.bar_branch_name}\n📍 {bar.address}\n{bar.bar.website}\n\n"
ya = "https://yandex.ru/maps/"
maps_link = f"{ya}?ll={bar.point}&z=16&text={bar.bar.name.replace(' ', '%20')}"
address = md.hlink(bar.address, maps_link)
address_text = f"📍 {address}"
text += f"🍻 {bar.bar_branch_name}\n{address_text}\n\n{bar.bar.website}\n\n\n"
response_text = "Сейчас мы знаем о следующих адресах:\n\n" + text

await message.reply(response_text, reply_markup=markup)
await message.reply(
response_text,
reply_markup=markup,
parse_mode="HTML",
disable_web_page_preview=True,
)

await state.finish()


class BarBranchGeo(BaseLogic):
def _register(self, dp: Dispatcher) -> None:
dp.register_message_handler(self.geo_bar_start, commands="geo_bar")
dp.register_message_handler(self.geo_bar_step2, state=GeoBarForm.bar)
dp.register_message_handler(
self.geo_bar_finish,
state=GeoBarForm.search_type,
content_types=types.ContentTypes.LOCATION | types.ContentTypes.TEXT,
)

@staticmethod
async def geo_bar_start(message: types.Message) -> None:
await GeoBarForm.bar.set()

markup = types.ReplyKeyboardMarkup(resize_keyboard=True, selective=True)
bars = get_bars().values_list("name", flat=True)
markup.add(*bars)
markup.add("Любой бар")

await message.reply("Выбери бар (кнопкой)", reply_markup=markup)

@staticmethod
async def geo_bar_step2(message: types.Message, state: FSMContext) -> None:
async with state.proxy() as data:
if message.text == "Любой бар":
data["bar"] = None
else:
data["bar"] = message.text
await GeoBarForm.next()

markup = types.ReplyKeyboardMarkup(resize_keyboard=True, selective=True)
button_geo = types.KeyboardButton("📍 Отправить геопозицию", request_location=True)
markup.add(button_geo)
await message.reply("Отправьте геопозицию или введите адрес", reply_markup=markup)

@staticmethod
async def geo_bar_finish(message: types.Message, state: FSMContext) -> None:
async with state.proxy() as data:
if message.location:
data["search_type"] = "location"
location_user = Point(message.location.longitude, message.location.latitude)

else:
data["search_type"] = "address"
geo = YandexMapGeo()
point = geo.geocode(message.text)
if point:
location_user = point

bars_branch = get_bars_branches(data["bar"])
markup = types.ReplyKeyboardRemove()
distances = {}
for bar in bars_branch:
point_bar = bar.point.split(",")

location_bar = Point(*point_bar)
distance = get_distance(location_user, location_bar)
distances[bar] = round(distance / 1000, 1)

distances_dict = dict(Counter(distances))
distances_tuple = sorted(distances_dict.items(), key=itemgetter(1))
distances_sorted = dict(distances_tuple[:3])

if len(distances_sorted) == 0:
response_text = "К сожалению, мы не смогли ничего найти, попробуйте еще раз."
else:
text = ""
for bar in distances_sorted:
ya = "https://yandex.ru/maps/"
maps_link = f"{ya}?ll={bar.point}&z=16&text={bar.bar.name.replace(' ', '%20')}"
address = md.hlink(bar.address, maps_link)
address_text = f"📍 {address}\n\n"
name_text = f"🍻 {bar.bar_branch_name} ~{distances_sorted[bar]} км\n"
website_text = f"{bar.bar.website}\n\n\n"
text += name_text + address_text + website_text
response_text = "Самые близкие бары:\n\n" + text

await message.reply(
response_text,
reply_markup=markup,
parse_mode="HTML",
disable_web_page_preview=True,
)

await state.finish()
1 change: 1 addition & 0 deletions bot/logics/on_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ async def set_default_commands(dp: Dispatcher) -> None:
types.BotCommand("filter", "Поиск пива"),
types.BotCommand("bars", "Список баров"),
types.BotCommand("addresses", "Адреса баров"),
types.BotCommand("geo_bar", "Ближайшие бары"),
types.BotCommand("help", "Помощь"),
]
print("### The default COMMANDS setup has been successfully completed")
Expand Down
2 changes: 2 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@
DB_NAME = os.getenv("DB_NAME", "db")

BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
GRAPHHOPPER_KEY = os.getenv("GRAPHHOPPER_KEY")
YANDEXMAP_KEY = os.getenv("YANDEXMAP_KEY")
4 changes: 3 additions & 1 deletion dev.env_template
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ DJANGO_SECRET_KEY=django_secret_key
DJANGO_ALLOWED_HOSTS=localhost 0.0.0.0 127.0.0.1 [::1]
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost http://0.0.0.0 http://127.0.0.1

TELEGRAM_BOT_TOKEN=telegram_bot_token
TELEGRAM_BOT_TOKEN=token
GRAPHHOPPER_KEY=token
YANDEXMAP_KEY=token

DB_HOST=localhost
DB_USER=django
Expand Down
Loading

0 comments on commit 2a3ce02

Please sign in to comment.