From 2fd436b7edf8aceb45c503919ffb21eab466efb6 Mon Sep 17 00:00:00 2001 From: ilia Date: Thu, 25 May 2023 17:12:39 +0300 Subject: [PATCH] add route generation route --- passfinder/events/api/consts.py | 226 ++++++++++++++++++ passfinder/events/api/serializers.py | 17 +- passfinder/events/api/views.py | 60 ++--- ...tauranttohotel_nearestrestauranttoevent.py | 57 +++++ passfinder/recomendations/models.py | 31 ++- passfinder/recomendations/service/service.py | 188 ++++++++++++--- 6 files changed, 514 insertions(+), 65 deletions(-) create mode 100644 passfinder/events/api/consts.py create mode 100644 passfinder/recomendations/migrations/0006_nearestrestauranttohotel_nearestrestauranttoevent.py diff --git a/passfinder/events/api/consts.py b/passfinder/events/api/consts.py new file mode 100644 index 0000000..eaa33b2 --- /dev/null +++ b/passfinder/events/api/consts.py @@ -0,0 +1,226 @@ +city_in_hotels = ['Абзаково', + 'Абрамовка', + 'Абрау-Дюрсо', + 'Адлер', + 'Азов', + 'Аксай', + 'Альметьевск', + 'Анапа', + 'Андрианово', + 'Арамиль', + 'Арзамас', + 'Арнеево', + 'Архипо-Осиповка', + 'Бабкино', + 'Базы отдыха ВТО', + 'Балашиха', + 'Батайск', + 'Беличье', + 'Белореченск', + 'Бердск', + 'Бердяш', + 'Березовка', + 'Бжид', + 'Битца', + 'Благовещенская', + 'Болтино', + 'Большой Сочи', + 'Бор, Нижегородская область', + 'Борисово', + 'Борносово', + 'Будённовск', + 'Вардане', + 'Васильево, Ленинградская область', + 'Васкелово', + 'Вербилки', + 'Веселовка, Краснодарский край', + 'Видное', + 'Витязево', + 'Владимировка', + 'Внуково', + 'Воскресенск', + 'Вотря', + 'Всеволожск', + 'Всходы', + 'Выборг', + 'Вырубово', + 'Гатчина', + 'Гвардейское', + 'Геленджик', + 'Голиково', + 'Головинка Краснодарский край', + 'Голубицкая', + 'Горки', + 'Городец', + 'Горячий ключ', + 'Григорчиково', + 'Гуамка', + 'Д/О Авангард', + 'Дагомыс', + 'Джемете', + 'Джубга', + 'Дзержинск', + 'Дивеево', + 'Дивногорье, Краснодарский край', + 'Дивноморское', + 'Дмитров', + 'Домодедово', + 'Дракино', + 'Дранишники', + 'Дубечино', + 'Егорьевск', + 'Ейск', + 'Екатеринбург', + 'Ершово', + 'Ессентуки', + 'Железноводск', + 'Жуковский', + 'За Родину', + 'Звенигород', + 'Зеленая поляна', + 'Ивантеевка', + 'Ильичево', + 'Иннолово ', + 'Иноземцево', + 'Исаково', + 'Истра', + 'Кабардинка', + 'Казань', + 'Каменск-Шахтинский', + 'Каневская', + 'Кингисепп', + 'Кисловодск', + 'Клин', + 'Коломна', + 'Коробицыно', + 'Королев', + 'Косулино', + 'Котельники', + 'Красная Горка', + 'Красная Поляна', + 'Красногорск', + 'Краснодар', + 'Красный Колос', + 'Кудряшовский', + 'Курово', + 'Кусимовского Рудника', + 'Кучугуры', + 'Лабинск', + 'Лазаревское', + 'Лермонтово', + 'Лесной городок', + 'Лодейное Поле', + 'Лоо', + 'Лосево', + 'Люберцы', + 'Магнитогорск', + 'Малые Решники', + 'Марьино, Ленинградская область', + 'Маяковского', + 'Мезмай', + 'Мещерино', + 'Миасс', + 'Минеральные Воды', + 'Мистолово', + 'Мишуткино', + 'Можайск', + 'Москва', + 'Мостовской', + 'Мытищи', + 'Набережные Челны', + 'Наро-Фоминск', + 'Нарынка', + 'Небуг', + 'Нестерово', + 'Нижний Новгород', + 'Нижний Тагил', + 'Новая', + 'Новоабзаково', + 'Нововолково', + 'Новомихайловский', + 'Новороссийск', + 'Новосибирск', + 'Новочеркасск', + 'Новый путь', + 'Ногинск', + 'Нурлат', + 'Овсяники', + 'Одинцово', + 'Озеры', + 'Оксино', + 'Октябрьский, Московская область', + 'Ольгинка', + 'Остров, Московская область', + 'Павловск', + 'Падиково', + 'Пересвет', + 'Платформа 69-й километр, Сосновское сельское поселение', + 'Подольск', + 'Подпорожье', + 'Полтавская', + 'Приморско-Ахтарск', + 'Приозерск', + 'Прохорово', + 'Пушкино', + 'Пятигорск', + 'Раменское', + 'Реутов', + 'Рождествено', + 'Роза Хутор', + 'Ростов-на-Дону', + 'Рощино', + 'Руза', + 'Санкт-Петербург', + 'Светлое', + 'Светлый', + 'Свирица', + 'Сергиев Посад', + 'Серпухов', + 'Симагино', + 'Сириус', + 'Скоково', + 'Снегири', + 'Солнечногорск', + 'Солохаул', + 'Сосново', + 'Сосновый Бор, Ленинградская область', + 'Сосновый Бор, Московская область', + 'Софрино', + 'Сочи', + 'Ставрополь', + 'Станица Динская', + 'Станица Должанская', + 'Старая Руза', + 'Степаньково', + 'Суйда', + 'Сукко', + 'Супсех', + 'Таганрог', + 'Тарасово', + 'Тимашевск', + 'Тихвин', + 'Тихорецк', + 'Тобольск', + 'Туапсе', + 'Тучково', + 'Тюмень', + 'Увильды ', + 'Углегорский', + 'Удельная', + 'Усть-Койсуг', + 'Усть-Лабинск', + 'Уфа', + 'Ушаки', + 'Фрязино', + 'Хадыженск', + 'Химки', + 'Хоста', + 'Чебаркуль', + 'Челябинск', + 'Чехов, Сахалинская область', + 'Чудская', + 'Шахты', + 'Широкая балка', + 'Щёлково', + 'Эсто-Садок', + 'Якорная щель'] \ No newline at end of file diff --git a/passfinder/events/api/serializers.py b/passfinder/events/api/serializers.py index 52b6bed..837a687 100644 --- a/passfinder/events/api/serializers.py +++ b/passfinder/events/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from rest_framework.generics import get_object_or_404 -from passfinder.events.models import Hotel, HotelPhone, City, Event, BasePoint, Region +from passfinder.events.models import Hotel, HotelPhone, City, Event, BasePoint, Region, Restaurant class HotelPhoneSerializer(serializers.ModelSerializer): @@ -49,7 +49,7 @@ class RouteSerializer(serializers.Serializer): class RouteInputSerializer(serializers.Serializer): date_from = serializers.DateField(required=False, allow_null=True) date_to = serializers.DateField(required=False, allow_null=True) - region = serializers.CharField( + city = serializers.CharField( min_length=24, max_length=24, required=False, allow_blank=True, allow_null=True ) @@ -106,3 +106,16 @@ def validate(self, data): class InputRouteSerializer(serializers.Serializer): points = serializers.ListSerializer(child=InputRoutePointSerializer()) + + +class ResaurantSerializer(serializers.ModelSerializer): + class Meta: + model = Restaurant + exclude = ('phones', ) + + +class ObjectRouteSerializer(serializers.Serializer): + lat = serializers.FloatField() + lon = serializers.FloatField() + title = serializers.CharField() + description = serializers.CharField() diff --git a/passfinder/events/api/views.py b/passfinder/events/api/views.py index ef2d436..c515afa 100644 --- a/passfinder/events/api/views.py +++ b/passfinder/events/api/views.py @@ -1,6 +1,12 @@ from rest_framework.generics import GenericAPIView, ListAPIView, get_object_or_404 from rest_framework.response import Response from drf_spectacular.utils import extend_schema +from django.db.models import Count +from random import choice +from passfinder.recomendations.service.service import generate_tour +from datetime import timedelta, datetime +from .consts import * + from passfinder.events.api.serializers import ( PointSerializer, @@ -46,34 +52,34 @@ def post(self, request): serializer = RouteInputSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.data - region = data["region"] - routes = [] - if region: - region = get_object_or_404(Region, oid=region) - for _ in range(2): - routes.append( - { - "name": "bebra", - "date": data["date_from"], - "description": "bebra bebra bebra", - "points": PointSerializer(many=True).to_representation( - region.points.all().order_by("?")[:10] - ), - } - ) + city_id = data["city"] + start_date = datetime.strptime(data['date_from'], '%Y-%m-%d') + end_date = datetime.strptime(data['date_to'], '%Y-%m-%d') + region = None + + if city_id: + region = get_object_or_404(City, oid=city_id) else: - for _ in range(10): - routes.append( - { - "name": "bebra", - "date": data["date_from"], - "description": "bebra bebra bebra", - "points": PointSerializer(many=True).to_representation( - BasePoint.objects.order_by("?")[:10] - ), - } - ) - return Response(data=routes) + region = choice(City.objects.annotate(points_count=Count('points')).filter(title__in=city_in_hotels)) + + if not start_date and end_date: + tour_length = choice([timedelta(days=i) for i in range(1, 4)]) + start_date = end_date - tour_length + if not end_date and start_date: + tour_length = choice([timedelta(days=i) for i in range(1, 4)]) + end_date = end_date + tour_length + if not end_date and not start_date: + max_date = datetime.now() + timedelta(days=15) + start_date = choice([max_date - timedelta(days=i) for i in range(1, 5)]) + tour_length = choice([timedelta(days=i) for i in range(1, 4)]) + end_date = start_date + tour_length + + print(request.user, region, start_date, end_date) + + tour = generate_tour(request.user, region, start_date, end_date) + print(len(tour[1])) + + return Response(data=tour[0]) class ListRegionApiView(ListAPIView): diff --git a/passfinder/recomendations/migrations/0006_nearestrestauranttohotel_nearestrestauranttoevent.py b/passfinder/recomendations/migrations/0006_nearestrestauranttohotel_nearestrestauranttoevent.py new file mode 100644 index 0000000..e0ce3d8 --- /dev/null +++ b/passfinder/recomendations/migrations/0006_nearestrestauranttohotel_nearestrestauranttoevent.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.1 on 2023-05-24 15:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0019_category_pointcategory_delete_tag"), + ("recomendations", "0005_userpreferences_prefferred_attractions_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="NearestRestaurantToHotel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "hotel", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="events.hotel" + ), + ), + ("restaurants", models.ManyToManyField(to="events.restaurant")), + ], + ), + migrations.CreateModel( + name="NearestRestaurantToEvent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="events.event" + ), + ), + ("restaurants", models.ManyToManyField(to="events.restaurant")), + ], + ), + ] diff --git a/passfinder/recomendations/models.py b/passfinder/recomendations/models.py index fb0d0dc..b200745 100644 --- a/passfinder/recomendations/models.py +++ b/passfinder/recomendations/models.py @@ -1,6 +1,6 @@ from django.db import models from passfinder.users.models import User -from passfinder.events.models import Event, Hotel +from passfinder.events.models import Event, Hotel, Restaurant class UserPreferences(models.Model): @@ -27,7 +27,36 @@ class NearestEvent(models.Model): event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='nearest_model_rel') nearest = models.ManyToManyField(Event, related_name='nearest_model_rev_rel') +""" +from passfinder.recomendations.service.service import generate_tour + +from passfinder.users.models import User + +from passfinder.events.models import City +from datetime import datetime + +start_date = datetime(year=2023, month=6, day=10) +end_date = datetime(year=2023, month=6, day=13) + +c = City.objects.get(title='Таганрог') + +u = User.objects.all()[0] + +generate_tour(u, c, start_date, end_date) + +""" class NearestHotel(models.Model): hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name='nearest_hotel_rel') nearest_events = models.ManyToManyField(Event, related_name='nearest_hotel_rev_rel') + + +class NearestRestaurantToEvent(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + restaurants = models.ManyToManyField(Restaurant) + + + +class NearestRestaurantToHotel(models.Model): + hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE) + restaurants = models.ManyToManyField(Restaurant) diff --git a/passfinder/recomendations/service/service.py b/passfinder/recomendations/service/service.py index c66cb96..22f8a20 100644 --- a/passfinder/recomendations/service/service.py +++ b/passfinder/recomendations/service/service.py @@ -1,8 +1,9 @@ from annoy import AnnoyIndex from .mapping.mapping import * from .models.models import * -from passfinder.events.models import Event, Region, Hotel, BasePoint, City -from passfinder.recomendations.models import UserPreferences, NearestEvent, NearestHotel +from passfinder.events.models import Event, Region, Hotel, BasePoint, City, Restaurant +from passfinder.events.api.serializers import HotelSerializer, EventSerializer, ResaurantSerializer, ObjectRouteSerializer +from passfinder.recomendations.models import * from random import choice, sample from collections import Counter from passfinder.users.models import User @@ -10,6 +11,7 @@ from django.db.models import Q from geopy.distance import geodesic as GD from datetime import timedelta, time, datetime +from gevent.pool import Pool def get_nearest_(instance_model, model_type, mapping, rev_mapping, nearest_n, ml_model): @@ -250,14 +252,14 @@ def get_personal_movies_recommendation(user): def dist_func(event1: Event, event2: Event): - # cords1 = [event1.lat, event1.lon] - # cords2 = [event2.lat, event2.lon] - # try: - # dist = GD(cords1, cords2).km - # return dist - # except: - # return 1000000 - return (event1.lon - event2.lon) ** 2 + (event1.lat - event2.lat) ** 2 + cords1 = [event1.lat, event1.lon] + cords2 = [event2.lat, event2.lon] + try: + dist = GD(cords1, cords2).km + return dist + except: + return 1000000 + #return (event1.lon - event2.lon) ** 2 + (event1.lat - event2.lat) ** 2 def generate_nearest(): @@ -288,6 +290,26 @@ def generate_hotel_nearest(): print(i) +def generate_nearest_restaurants(): + rests = list(Restaurant.objects.all()) + for i, event in enumerate(Event.objects.all()): + sorted_rests = list(sorted(rests.copy(), key=lambda x: dist_func(x, event))) + nr = NearestRestaurantToEvent.objects.create(event=event) + nr.restaurants.set(sorted_rests[0:20]) + nr.save() + if i % 100 == 0: + print(i) + + for i, hotel in enumerate(Hotel.objects.all()): + sorted_rests = list(sorted(rests.copy(), key=lambda x: dist_func(x, hotel))) + nr = NearestRestaurantToHotel.objects.create(hotel=hotel) + nr.restaurants.set(sorted_rests[0:20]) + nr.save() + if i % 100 == 0: + print(i) + + + def match_points(): regions = list(City.objects.all()) for i, point in enumerate(Event.objects.all()): @@ -339,14 +361,18 @@ def calculate_favorite_metric(event: Event, user: User): def get_nearest_favorite( events: Iterable[Event], user: User, exclude_events: Iterable[Event] = [] ): + print(events) first_event = None for candidate in events: if candidate not in exclude_events: first_event = candidate break - - result = first_event - result_min = calculate_favorite_metric(result, user) + + if first_event is None: + result = events[0] + else: + result = first_event + result_min = 1000000 for event in events: if event in exclude_events: continue @@ -372,50 +398,142 @@ def generate_route(point1: BasePoint, point2: BasePoint): time = time_func(distance) return { "type": "transition", - "from": point1, - "to": point2, "distance": distance, - "time": time, + "time": time.seconds, } def generate_point(point: BasePoint): + event_data = ObjectRouteSerializer(point).data return { "type": "point", - "point": point, - "point_type": "", - "time": timedelta(minutes=90+choice(range(-10, 90, 10))) + "point": event_data, + "point_type": "point", + "time": timedelta(minutes=90+choice(range(-10, 90, 10))).seconds } -def generate_path(region: Region, user: User): + +def generate_restaurant(point: BasePoint): + rest_data = ObjectRouteSerializer(point).data + + return { + "type": "point", + "point": rest_data, + "point_type": "restaurant", + "time": timedelta(minutes=90+choice(range(-10, 90, 10))).seconds + } + + +def generate_multiple_tours(user: User, city: City, start_date: datetime.date, end_date: datetime.date): + hotels = sample(list(Hotel.objects.filter(city=city)), 5) + pool = Pool(5) + return pool.map(generate_tour, [(user, start_date, end_date, hotel) for hotel in hotels]) + + +def generate_tour(user: User, city: City, start_date: datetime.date, end_date: datetime.date): + print("start hotel") + hotel = choice(list(Hotel.objects.filter(city=city))) + print("end hotel") + current_date = start_date + paths, points = [], [] + + while current_date < end_date: + print("start day gen") + local_points, local_paths = generate_path(user, points, hotel) + points.extend(local_points) + paths.append( + { + 'date': current_date, + 'paths': local_paths + } + ) + print("end day gen") + + current_date += timedelta(days=1) + + return paths, points + + +def generate_hotel(hotel: Hotel): + hotel_data = ObjectRouteSerializer(hotel).data + return { + "type": "point", + "point": hotel_data, + "point_type": "hotel", + "time": timedelta(minutes=90+choice(range(-10, 90, 10))).seconds + } + + +def generate_path(user: User, disallowed_points: Iterable[BasePoint], hotel: Hotel): # region_events = Event.objects.filter(region=region) - hotel = filter_hotel(region, user, []) + #candidates = NearestHotel.objects.get(hotel=hotel).nearest_events.all() + allowed_types = ['museum', 'attraction'] - candidates = NearestHotel.objects.get(hotel=hotel).nearest_events.all() - - start_point = get_nearest_favorite(candidates, user, []) - - candidates = NearestEvent.objects.get(event=start_point).nearest.all() + print("start start point gen") + start_point = NearestRestaurantToHotel.objects.get(hotel=hotel).restaurants.first() + print("end start point gen") + print("start first cand gen") + candidates = list(filter(lambda x: x.type in allowed_types, map(lambda x: x.event, start_point.nearestrestauranttoevent_set.all()[0:100]))) + print("end first cand gen") points = [start_point] - - path = [generate_point(points[-1])] + path = [ + generate_hotel(hotel), + generate_route(start_point, hotel), + generate_restaurant(points[-1]) + ] start_time = datetime.combine(datetime.now(), time(hour=10)) - while start_time.hour < 22: - candidates = NearestEvent.objects.get(event=points[-1]).nearest.all() - points.append(get_nearest_favorite(candidates, user, points)) + how_many_eat = 1 + + while start_time.hour < 22: + print(start_time) + if (start_time.hour > 13 and how_many_eat == 1) or (start_time.hour > 20 and how_many_eat == 2): + print("start rest event gen") + point = NearestRestaurantToEvent.objects.get(event=points[-1]).restaurants.all()[0] + points.append(point) + candidates = list(filter(lambda x: x.type in allowed_types, map(lambda x: x.event, point.nearestrestauranttoevent_set.all()[0:100]))) + if not len(candidates): + candidates = list(map(lambda x: x.event, point.nearestrestauranttoevent_set.all()[0:100])) + + path.append(generate_restaurant(points[-1])) + start_time += timedelta(seconds=path[-1]['time']) + how_many_eat += 1 + print("start rest event gen") + continue + + if start_time.hour > 17: + allowed_types = ['play', 'concert', 'movie'] + + print("start events gen") + if candidates is None: + candidates = NearestEvent.objects.get(event=points[-1]).nearest.filter(type__in=allowed_types) + if not len(candidates): + candidates = NearestEvent.objects.get(event=points[-1]).nearest.all() + print("end events gen") + + print("start events select") + try: + print(points) + points.append(get_nearest_favorite(candidates, user, points + disallowed_points)) + + except AttributeError: + points.append(get_nearest_favorite(candidates, user, points)) + print("end events select") + + print("start route gen") transition_route = generate_route(points[-1], points[-2]) - start_time += transition_route["time"] + start_time += timedelta(seconds=transition_route["time"]) point_route = generate_point(points[-1]) - start_time += point_route["time"] + start_time += timedelta(seconds=point_route["time"]) path.extend([transition_route, point_route]) - - return hotel, points, path + candidates = None + print("end route gen") + return points, path def calculate_distance(sample1: Event, samples: Iterable[Event], model: AnnoyIndex, rev_mapping):