From ba53689ab4b5f5fe24b57101bc2b86946aa354cd Mon Sep 17 00:00:00 2001 From: ViktorSemericov Date: Mon, 27 Oct 2025 11:48:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database.py | 86 +++++++++ new_parser_test.py | 389 +++++++++++++++++++++++++++++++++++++ places_from_xlsx.py | 7 + route.py | 454 ++++++++++++++++++++++++++++++++++++++++++++ wrapper.py | 26 +++ 5 files changed, 962 insertions(+) create mode 100644 database.py create mode 100644 new_parser_test.py create mode 100644 places_from_xlsx.py create mode 100644 route.py create mode 100644 wrapper.py diff --git a/database.py b/database.py new file mode 100644 index 0000000..ef4dd71 --- /dev/null +++ b/database.py @@ -0,0 +1,86 @@ +import json + + +def search_database(database_file, query): + """ + Поиск по базе данных с группировкой тегов по приоритетам. + + Args: + database: список словарей с данными + query: список списков тегов, например [["Памятник"], ["Архитектура"]] + + Returns: + список найденных записей с добавленными полями priority и измененным category_id + """ + with open(database_file, 'r', encoding='utf-8') as f: + database = json.load(f) + tag_mapping = {} + tag_counter = 1 + + # Создаем маппинг тегов + any_mode = False + for priority_group_idx, tag_group in enumerate(query, start=1): + if any_mode: + tag_counter+=1 + if 'ANY' in tag_group: + tag_group.remove('ANY') + any_mode = True + else: + any_mode = False + for tag in tag_group: + if tag not in tag_mapping: + tag_mapping[tag] = { + 'priority': priority_group_idx, + 'tag_number': tag_counter + } + if not any_mode: + tag_counter += 1 + + # Поиск и обработка записей + results = [] + seen_ids = set() + + for entry in database: + categories = entry.get('category_id') + categories =list(categories.split(', ')) + found = False + for ctg in categories: + if ctg in tag_mapping: + found = True + category= ctg + break + if found: + entry_id = (entry.get('coordinate'), entry.get('title')) + + if entry_id not in seen_ids: + seen_ids.add(entry_id) + result_entry = entry.copy() + result_entry['type'] = tag_mapping[category]['tag_number'] + result_entry['priority'] = tag_mapping[category]['priority'] + results.append(result_entry) + + # Сортируем по приоритету + tag_priority = {v['tag_number']: v['priority'] for v in tag_mapping.values()} + # Then verify no conflicts + if len(set((v['tag_number'], v['priority']) for v in tag_mapping.values())) != len(tag_priority): + raise ValueError("Conflicting priorities for same tag_number") + return results,tag_priority + + +# Пример использования +if __name__ == "__main__": + # Загрузка базы данных из файла + with open('output.json', 'r', encoding='utf-8') as f: + database = json.load(f) + + # Запрос с группами тегов + query = [["Памятник", "Музей"], ["Архитектура"], ["Парк", "Сквер"]] + + # Выполнение поиска + results = search_database(database, query) + print(results) + # Сохранение результатов + with open('search_results.json', 'w', encoding='utf-8') as f: + json.dump(results, f, ensure_ascii=False, indent=2) + + print(f"Найдено записей: {len(results)}") diff --git a/new_parser_test.py b/new_parser_test.py new file mode 100644 index 0000000..bd94ab8 --- /dev/null +++ b/new_parser_test.py @@ -0,0 +1,389 @@ +import os +import json +import re +import requests +from typing import Dict, Any, List, Optional +from datetime import datetime, timedelta + +API_KEY = "AIzaSyBXGBGH5NDY8L_jVmq2zb4i8xYEV2qN-48" +API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent" + +# Базовый список доступных тегов +TAG_LIST = [ + "Памятник", "Архитектура", "Мозаика", "Панно", + "Парк", "Водоем", "Озеро", "Ботанический сад", + "Архитектура", "Площадь", "Набережная", "Канатная дорога", + "Сквер", "Музей", "Собор", "Церковь", + "Храм", "Театр", "Выставка", "Мост", "Художественное пространство", "Вокзал", "Фонтан", "Лестница", "Кинотеатр", + "Дом Культуры", "Планетарий", "Галерея", "Ресторан", "Бар", "Бистро", "Кафе" +] + + +def build_extraction_prompt(conversation_history: List[Dict[str, str]], tag_list: List[str]) -> str: + """ + Строим промпт для ИИ-парсера с историей диалога + """ + example_user_1 = "Я хочу культурно провести время, у меня 2 часа, я сейчас на улице Доброй около дома 1." + example_json_1 = { + "tags": [["Музей", "Галерея", "Театр","Выставка","ANY"]], + "user_location": "улица Добрая, дом 1", + "time": 120 + } + example_user_2 = "Я хочу сходить в музей пройти по парку и где-нибудь поесть, у меня 3.5 часа, я на Коминтерна, 115" + example_json_2 = { + "tags": [["Музей", "Парк"], ["Ресторан", "Бистро", "Кафе", "ANY"]], + "user_location": "улица Коминтерна, дом 115", + "time": 120 + } + instr = f""" +Ты — ИИ-парсер на русском языке. +Задача: из диалога с пользователем извлечь строго JSON c полями: + +- tags: упорядоченный список релевантных тегов ТОЛЬКО из данного tag_list (не придумывай новых), теги должны быть сгруппированы по приоритету посещения +- user_location: строка с текущим местоположением пользователя (примерный адрес), должен содержать название улицы/площади/... Но не может быть ориентиром на объект без конкретного адреса или чем-то абстрактным: дома, у памятника Пушкину, у станции метро, на автобусной остановке... не подходят, но я около площади Революции подходит. +- time: целое число минут на прогулку (преобразуй выражения типа "2 часа" -> 120, "полчаса" -> 30 и т.п.). Пользователь может указать до скольки он свободен тогда используй фиксированный формат until <время в 24 формате HH:MM>. 'Я хочу погулять до 6 вечера' -> тогда в поле время 'until 18:00'. + +Работа с тегами: +Группируй теги по ПРИОРИТЕТУ посещения: +- Если пользователь говорит "сначала музей, потом кофе" -> [['Музей'], ['Кофейня']] +- Если "хочу в музей и кофе" (без явного порядка) -> [['Музей', 'Кофейня']] +- Если неопределённые предпочтения "погулять по интересным местам", добавь ключевое слово 'ANY' к группе подходящих тегов. +- Запрос пользователя может быть абстрактным "я хочу погулять по улице". Подбери релевантные теги на открытом воздухе (т.к. погулять можно интерпретировать как походить по улице не заходя куда-либо) например: [['Архитектура','Сквер','Площадь', 'ANY']]. Запрос может быть абстрактным, но не может быть полностью неопределенным. Примеры неопределенных ответов: "Хочу куда-нибудь сходить", "Я не знаю куда сходить", "Куда мне сходить?" +- Указывай все теги из tag_list которые явно подходят!!! Примеры могут быть неполными! + +ИСПОЛЬЗУЙ ТОЛЬКО ТЕГИ ИЗ ДАННОГО СПИСКА! + +Требования: +- Верни ТОЛЬКО валидный JSON без префиксов, комментариев и форматирования в кодовых блоках. +- Если в диалоге с пользователем нет данных для какого-либо поля, поставь null в этом поле. +- Не добавляй никаких дополнительных полей, только: tags, user_location, time. +- tags должны быть подмножеством из tag_list. Не включай нерелевантные теги. + +tag_list = {json.dumps(tag_list, ensure_ascii=False, indent=0)} + +Пример 1: +Пользователь: "{example_user_1}" +Ожидаемый JSON: +{json.dumps(example_json_1, ensure_ascii=False, indent=2)} + +Пример 2: +Пользователь: "{example_user_2}" +Ожидаемый JSON: +{json.dumps(example_json_2, ensure_ascii=False, indent=2)} + + +История диалога: +""" + + for msg in conversation_history: + role = msg["role"] + content = msg["content"] + if role == "user": + instr += f"\nПользователь: \"{content}\"" + elif role == "assistant": + instr += f"\nАссистент: \"{content}\"" + + instr += "\n\nТеперь извлеки JSON из диалога выше." + + return instr.strip() + + +def build_conversational_prompt(conversation_history: List[Dict[str, str]], missing: List[str], + tag_list: List[str]) -> str: + """ + Строим промпт для генерации уточняющего вопроса + """ + missing_desc = { + "tags": "какие места/категории интересны", + "user_location": "текущее местоположение (адрес)", + "time": "время на прогулку" + } + + missing_fields_str = ", ".join([missing_desc[f] for f in missing]) + + instr = f""" +Ты — вежливый ИИ-ассистент, который помогает пользователю спланировать прогулку. +Твоя задача — задать краткие вопросы, чтобы уточнить недостающую информацию. + +Недостающие данные: {missing_fields_str} + +Правила: +- Задай ТОЛЬКО простык, вежливый и краткий вопрос. +- НЕ давай рекомендаций, комментариев или предложений. +- НЕ предлагай варианты, кроме случаев когда нужно выбрать категории (tags). +- Если нужно уточнить категории (tags), предложи категории ИЗ ЭТОГО СПИСКА: {json.dumps(tag_list, ensure_ascii=False)} +- Верни ТОЛЬКО текст вопроса без дополнительных пояснений. + +История диалога: +""" + + for msg in conversation_history: + role = msg["role"] + content = msg["content"] + if role == "user": + instr += f"\nПользователь: {content}" + elif role == "assistant": + instr += f"\nАссистент: {content}" + + instr += "\n\nТеперь задай уточняющий вопрос:" + + return instr.strip() + + +def call_gemini(prompt: str) -> str: + headers = { + "Content-Type": "application/json", + "x-goog-api-key": API_KEY, + } + + payload = { + "contents": [ + { + "role": "user", + "parts": [{"text": prompt}] + } + ] + } + + resp = requests.post(API_URL, headers=headers, json=payload, timeout=30) + resp.raise_for_status() + data = resp.json() + + # Извлекаем текст первого кандидата + text = "" + try: + text = data["candidates"][0]["content"]["parts"][0]["text"] + except Exception: + # Попытка fallback: иногда ответ может приходить иначе + text = json.dumps(data, ensure_ascii=False) + + return text + + +def strip_code_fences(text: str) -> str: + s = text.strip() + # Убираем ограду `````` или `````` + if s.startswith("```"): + # срезаем первые ``` + s = s[3:].lstrip() + # если указан язык (например, json), уберём первую строку + first_nl = s.find("\n") + if first_nl != -1: + lang = s[:first_nl].strip().lower() + # если это похоже на метку языка, отбрасываем её + if lang in ("json", "yaml", "yml", "xml", "markdown", "md", "txt"): + s = s[first_nl + 1:] + else: + # если метка не признана, всё равно продолжаем с текущим s + pass + # убираем завершающие ``` + if s.rstrip().endswith("```"): + s = s.rstrip()[:-3] + + return s.strip() + + +def extract_first_json(text: str): + # 1) снимаем ограду + s = strip_code_fences(text) + + # 2) пробуем распарсить как есть + try: + return json.loads(s) + except Exception: + pass + + # 3) ищем первый сбалансированный объект { ... } без рекурсивного regex + start = s.find("{") + while start != -1: + depth = 0 + in_str = False + esc = False + for i in range(start, len(s)): + ch = s[i] + if in_str: + if esc: + esc = False + elif ch == "\\": + esc = True + elif ch == '"': + in_str = False + else: + if ch == '"': + in_str = True + elif ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + cand = s[start:i + 1] + try: + return json.loads(cand) + except Exception: + break + # ищем следующий '{', если текущий блок не распарсился + start = s.find("{", start + 1) + + return None + + +def is_complete(result: Dict[str, Any]) -> bool: + if not isinstance(result, dict): + return False + + # Проверяем наличие ключей + for key in ["tags", "user_location", "time"]: + if key not in result: + return False + + # Проверяем содержимое + tags_ok = isinstance(result["tags"], list) and len(result["tags"]) > 0 + loc_ok = isinstance(result["user_location"], str) and len(result["user_location"].strip()) > 0 + + # time может быть int или строка формата "until HH:MM" + time_val = result["time"] + if isinstance(time_val, int) and time_val > 0: + time_ok = True + elif isinstance(time_val, str) and time_val.startswith("until "): + time_ok = True + else: + time_ok = False + + return tags_ok and loc_ok and time_ok + + +def missing_fields(result: Dict[str, Any]) -> List[str]: + missing = [] + + if not isinstance(result, dict): + return ["tags", "user_location", "time"] + + if "tags" not in result or not isinstance(result["tags"], list) or len(result["tags"]) == 0: + missing.append("tags") + + if "user_location" not in result or not isinstance(result["user_location"], str) or not result[ + "user_location"].strip(): + missing.append("user_location") + + time_val = result.get("time") + if isinstance(time_val, int) and time_val > 0: + pass # OK + elif isinstance(time_val, str) and time_val.startswith("until "): + pass # OK + else: + missing.append("time") + + return missing + + +def convert_time_to_minutes(time_val, current_time: datetime) -> int: + """ + Конвертирует значение времени в минуты. + Если time_val — это строка формата "until HH:MM", вычисляет разницу от текущего времени. + """ + if isinstance(time_val, int): + return time_val + + if isinstance(time_val, str) and time_val.startswith("until "): + time_str = time_val.replace("until ", "").strip() + try: + target_time = datetime.strptime(time_str, "%H:%M") + # Устанавливаем дату как текущую + target_datetime = current_time.replace( + hour=target_time.hour, + minute=target_time.minute, + second=0, + microsecond=0 + ) + + # Если целевое время уже прошло сегодня, предполагаем завтра + if target_datetime <= current_time: + target_datetime += timedelta(days=1) + + delta = target_datetime - current_time + minutes = int(delta.total_seconds() / 60) + return minutes + except ValueError: + return None + + return None + + +def extract_with_ai(conversation_history: List[Dict[str, str]]) -> Dict[str, Any]: + prompt = build_extraction_prompt(conversation_history, TAG_LIST) + model_text = call_gemini(prompt) + parsed = extract_first_json(model_text) or {} + return parsed + + +def ask_ai_for_clarification(conversation_history: List[Dict[str, str]], missing: List[str]) -> str: + """ + Генерируем уточняющий вопрос через ИИ + """ + prompt = build_conversational_prompt(conversation_history, missing, TAG_LIST) + question = call_gemini(prompt) + return question.strip() + + +def parse(): + if not API_KEY or API_KEY.startswith("<"): + raise RuntimeError("Установите переменную окружения GEMINI_API_KEY с вашим ключом API.") + + print( + "Опишите ваш запрос (например: 'Я хочу культурно провести время потом попить кофе, у меня 2 часа, я сейчас на улице Дальней около дома 8.')") + + conversation_history = [] + + # Первое сообщение от пользователя + user_input = input("> ").strip() + conversation_history.append({"role": "user", "content": user_input}) + + # Текущее время (Москва, UTC+3) + current_time = datetime.now() + + # Первая попытка извлечения + result = extract_with_ai(conversation_history) + + # Итеративный диалог + max_tries = 10 + tries = 0 + + while (not is_complete(result)) and tries < max_tries: + missing = missing_fields(result) + + if not missing: + break + + # Генерируем уточняющий вопрос через ИИ + ai_question = ask_ai_for_clarification(conversation_history, missing) + print(ai_question) + + conversation_history.append({"role": "assistant", "content": ai_question}) + + # Получаем ответ пользователя + user_response = input("> ").strip() + conversation_history.append({"role": "user", "content": user_response}) + + # Повторно извлекаем данные с учётом новой информации + result = extract_with_ai(conversation_history) + tries += 1 + + # Конвертируем время если нужно + if "time" in result and isinstance(result["time"], str): + minutes = convert_time_to_minutes(result["time"], current_time) + if minutes is not None: + result["time"] = minutes + + # Печатаем финальный JSON + out = { + "tags": result.get("tags", None), + "user_location": result.get("user_location", None), + "time": result.get("time", None), + } + return json.dumps(out, ensure_ascii=False, indent=2) + + +if __name__ == "__main__": + out = parse() + print(out) + diff --git a/places_from_xlsx.py b/places_from_xlsx.py new file mode 100644 index 0000000..c705ff1 --- /dev/null +++ b/places_from_xlsx.py @@ -0,0 +1,7 @@ +import pandas as pd + +# Читаем конкретный лист из Excel +df = pd.read_excel('cultural_objects_mnn_final.xlsx', sheet_name='list1') + +# Сохраняем в JSON с красивым форматированием +df.to_json('output.json', orient='records', force_ascii=False, indent=4) \ No newline at end of file diff --git a/route.py b/route.py new file mode 100644 index 0000000..4d4ce69 --- /dev/null +++ b/route.py @@ -0,0 +1,454 @@ +import math +import itertools +import requests +from typing import List, Tuple, Dict, Optional, Set + +class Point: + def __init__(self, coord: List[float], tag: str, visit_time: int): + self.coord = coord + self.tag = tag + self.visit_time = visit_time + self.matrix_index = None # Индекс точки в матрице расстояний + self.estimated_time = None # Оценочное время (перемещение + посещение) + +def haversine(coord1: List[float], coord2: List[float]) -> float: + """Calculate the great-circle distance between two points in kilometers.""" + lat1, lon1 = coord1 + lat2, lon2 = coord2 + R = 6371 # Earth radius in km + + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + a = (math.sin(dlat/2) * math.sin(dlat/2) + + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * + math.sin(dlon/2) * math.sin(dlon/2)) + return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + +def filter_points_by_time(start_coord: List[float], points: List[Point], total_time: int) -> List[Point]: + """Filter points based on straight-line distance and visit time.""" + filtered = [] + for point in points: + distance = haversine(start_coord, point.coord) + travel_time = distance * 10 # Assume 6 km/h walking speed (10 min/km) + point.estimated_time = travel_time + point.visit_time + if point.estimated_time <= total_time: + filtered.append(point) + return filtered + +def filter_points_by_tag_proximity(points: List[Point], max_per_tag: int = 10) -> List[Point]: + """For each tag, keep only the closest points (by estimated time).""" + # Group points by tag + tag_to_points = {} + for point in points: + if point.tag not in tag_to_points: + tag_to_points[point.tag] = [] + tag_to_points[point.tag].append(point) + + # For each tag, sort by estimated time and keep top max_per_tag + filtered_points = [] + for tag, tag_points in tag_to_points.items(): + # Sort by estimated time (ascending) + sorted_points = sorted(tag_points, key=lambda p: p.estimated_time) + # Keep at most max_per_tag points + kept_points = sorted_points[:max_per_tag] + filtered_points.extend(kept_points) + print(f"Tag '{tag}': kept {len(kept_points)} out of {len(tag_points)} points") + + return filtered_points + +def get_duration_matrix(points: List[List[float]]) -> Optional[Tuple[List[List[float]], List[List[float]]]]: + """Get duration matrix from server.""" + url = "https://ha1m-maap-pdmc.gw-1a.dockhost.net/table" + payload = {"points": points} + headers = {"content-type": "application/json"} + + try: + response = requests.post(url, json=payload, headers=headers, timeout=30) + if response.status_code == 200: + data = response.json() + return data.get("distances"), data.get("durations") + else: + print(f"Server error: {response.status_code}") + return None + except Exception as e: + print(f"Error requesting duration matrix: {e}") + return None + +def group_points_by_significance(points: List[Point], tag_importance: Dict[str, int]) -> Dict[int, List[Point]]: + """Group points by their importance level.""" + grouped = {} + for point in points: + importance = tag_importance.get(point.tag, float('inf')) + if importance not in grouped: + grouped[importance] = [] + grouped[importance].append(point) + return grouped + +def calculate_route_time_with_matrix(route: List[Point], start_coord: List[float], + duration_matrix: List[List[float]]) -> float: + """Calculate total time for a route using the duration matrix.""" + total_time = 0 + current_index = 0 # Start point index + + for point in route: + next_index = point.matrix_index + travel_time_seconds = duration_matrix[current_index][next_index] + travel_time_minutes = travel_time_seconds / 60.0 + total_time += travel_time_minutes + point.visit_time + current_index = next_index + + return total_time + +def check_tags_constraint(points: List[Point]) -> bool: + """Check if there are no more than 5 unique tags.""" + unique_tags = set(point.tag for point in points) + return len(unique_tags) <= 5 + +def generate_routes_exact_tags(grouped_points: Dict[int, List[Point]], + all_tags: Set[str], + tag_importance: Dict[str, int]) -> List[List[Point]]: + """Generate routes where each tag is visited exactly once using different coordinates.""" + # Create a mapping from tag to points + tag_to_points = {} + for points_list in grouped_points.values(): + for point in points_list: + if point.tag not in tag_to_points: + tag_to_points[point.tag] = [] + tag_to_points[point.tag].append(point) + + # For each tag, we need to select exactly one point + tag_selections = [] + for tag in all_tags: + tag_selections.append(tag_to_points[tag]) + + # Generate all combinations of points (one per tag) + all_routes = [] + print(len(list(itertools.product(*tag_selections)))) + for point_combination in itertools.product(*tag_selections): + + # Check if all points have unique coordinates + coords = [tuple(point.coord) for point in point_combination] + if len(coords) != len(set(coords)): + continue # Skip if any coordinates are duplicated + + # Group points by importance + points_by_importance = {} + for point in point_combination: + imp = tag_importance[point.tag] + if imp not in points_by_importance: + points_by_importance[imp] = [] + points_by_importance[imp].append(point) + + # Sort by importance + sorted_importances = sorted(points_by_importance.keys()) + + # Generate all permutations within each importance group + importance_groups = [points_by_importance[imp] for imp in sorted_importances] + + for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]): + route = [] + for group in ordering: + route.extend(group) + all_routes.append(route) + + return all_routes + +def generate_routes_with_repeats(grouped_points: Dict[int, List[Point]], + all_tags: Set[str], + tag_importance: Dict[str, int], + num_points: int) -> List[List[Point]]: + """Generate routes when we need to repeat tags to reach the required number of points, ensuring unique coordinates.""" + # Create a mapping from tag to points + tag_to_points = {} + for points_list in grouped_points.values(): + for point in points_list: + if point.tag not in tag_to_points: + tag_to_points[point.tag] = [] + tag_to_points[point.tag].append(point) + + all_routes = [] + + # First, select one point for each tag (mandatory points) + mandatory_selections = [tag_to_points[tag] for tag in all_tags] + + # Generate all combinations of mandatory points (one per tag) + for mandatory_combo in itertools.product(*mandatory_selections): + mandatory_points = list(mandatory_combo) + + # Check if mandatory points have unique coordinates + mandatory_coords = [tuple(point.coord) for point in mandatory_points] + if len(mandatory_coords) != len(set(mandatory_coords)): + continue # Skip if any coordinates are duplicated in mandatory points + + # We need to add (num_points - len(mandatory_points)) additional points + num_additional = num_points - len(mandatory_points) + + if num_additional == 0: + # We have exactly the right number of points + points_by_importance = {} + for point in mandatory_points: + imp = tag_importance[point.tag] + if imp not in points_by_importance: + points_by_importance[imp] = [] + points_by_importance[imp].append(point) + + sorted_importances = sorted(points_by_importance.keys()) + importance_groups = [points_by_importance[imp] for imp in sorted_importances] + + for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]): + route = [] + for group in ordering: + route.extend(group) + all_routes.append(route) + else: + # We need to add additional points (can be from any tag, including repeats) + # But we must ensure all coordinates are unique + + # Get all available points excluding mandatory points + all_available_points = [] + for points_list in grouped_points.values(): + all_available_points.extend(points_list) + + # Remove mandatory points from available points + available_points = [p for p in all_available_points if p not in mandatory_points] + + # Generate combinations of additional points + for additional_combo in itertools.combinations(available_points, num_additional): + # Check if additional points have unique coordinates and don't duplicate with mandatory + additional_coords = [tuple(point.coord) for point in additional_combo] + if len(additional_coords) != len(set(additional_coords)): + continue # Skip if any coordinates are duplicated in additional points + + # Check if additional points don't duplicate with mandatory points + all_coords = mandatory_coords + additional_coords + if len(all_coords) != len(set(all_coords)): + continue # Skip if any coordinates are duplicated between mandatory and additional + + full_route_candidate = mandatory_points + list(additional_combo) + + # Group by importance + points_by_importance = {} + for point in full_route_candidate: + imp = tag_importance[point.tag] + if imp not in points_by_importance: + points_by_importance[imp] = [] + points_by_importance[imp].append(point) + + sorted_importances = sorted(points_by_importance.keys()) + importance_groups = [points_by_importance[imp] for imp in sorted_importances] + + for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]): + route = [] + for group in ordering: + route.extend(group) + all_routes.append(route) + + return all_routes + +def form_point_list(data): + point_list =[] + for entry in data: + point = Point(list(map(float,entry['coordinate'].split(', '))),entry['type'],entry['time_to_visit']) + point_list.append(point) + return point_list + +def build_route(data, mapping,start_coord,total_time,n_nodes): + # Example input data - теперь с не более чем 5 уникальными тегами + + start_coord_test = [56.331576, 44.003277] + total_time_test = 180 # Увеличим время до 4 часов для большего выбора + points = form_point_list(data) + tag_importance =mapping + # Используем 3 уникальных тега для демонстрации + points_test = [ + Point([56.32448, 43.983546], "Памятник", 20), + Point([56.335607, 43.97481], "Архитектура", 20), + Point([56.313472, 43.990747], "Памятник", 20), + #Point([56.324157, 44.002696], "Памятник", 20), + #Point([56.316436, 43.994177], "Памятник", 20), + #Point([56.32377, 44.001879], "Памятник", 20), + #Point([56.329867, 43.99687], "Памятник", 20), + Point([56.311066, 43.94595], "Памятник", 20), + Point([56.333265, 43.972417], "Памятник", 20), + # Point([56.332166, 44.012111], "Памятник", 20), + #Point([56.326786, 44.006836], "Памятник", 20), + Point([56.330232, 44.010941], "Парк", 20), + Point([56.282221, 43.979263], "Парк", 20), + Point([56.277315, 43.921408], "Мозаика", 20), + Point([56.284829, 44.01893], "Парк", 20), + Point([56.308973, 43.99821], "Парк", 20), + Point([56.321545, 44.001921], "Парк", 20), + #Point([56.301798, 44.044003], "Мозаика", 20), + Point([56.268282, 43.919475], "Парк", 20), + Point([56.239625, 43.854551], "Парк", 20), + #Point([56.311214, 43.933981], "Парк", 20), + Point([56.314984, 44.007347], "Парк", 20), + Point([56.32509, 43.983433], "Парк", 20), + Point([56.27449, 43.973357], "Парк", 20), + Point([56.278073, 43.940886], "Парк", 20), + Point([56.358805, 43.825376], "Парк", 20), + Point([56.329995, 44.009444], "Памятник", 20), + Point([56.328551, 43.998718], "Памятник", 20), + Point([56.330355, 43.993105], "Архитектура", 20), + Point([56.321416, 43.973897], "Архитектура", 20), + # Point([56.327298, 44.005706], "Архитектура", 20), + #Point([56.328757, 43.998183], "Архитектура", 20), + # Point([56.328908, 43.995645], "Архитектура", 20), + Point([56.317578, 43.995805], "Архитектура", 20), + Point([56.329433, 44.012764], "Архитектура", 20), + Point([56.3301, 44.008831], "Архитектура", 20), + #Point([56.32995, 43.999495], "Архитектура", 20), + Point([56.327454, 44.041745], "Архитектура", 20), + #Point([56.328576, 44.004872], "Архитектура", 20), + Point([56.3275, 44.007658], "Архитектура", 20), + Point([56.330679, 44.013874], "Архитектура", 20), + # Point([56.331541, 44.001747], "Архитектура", 20), + # Point([56.335071, 43.974627], "Архитектура", 20), + #Point([56.317707, 43.995847], "Архитектура", 20), + #Point([56.323851, 43.985939], "Архитектура", 20), + Point([56.325701, 44.001527], "Архитектура", 20), + Point([56.328754, 43.998954], "Архитектура", 20), + #Point([56.323937, 43.990728], "Музей", 20), + #Point([56.2841, 43.84621], "Музей", 20), + #Point([56.328646, 44.028973], "Музей", 20), + Point([56.327391, 43.857522], "Мозаика", 20), + #Point([56.252239, 43.889066], "Мозаика", 20), + #Point([56.248436, 43.88106], "Мозаика", 20), + #Point([56.321257, 43.94545], "Мозаика", 20), + # Point([56.365284, 43.823251], "Мозаика", 20), + Point([56.294371, 43.912625], "Мозаика", 20), + #Point([56.241768, 43.859687], "Мозаика", 20), + #Point([56.300073, 43.938526], "Мозаика", 20), + #Point([56.229652, 43.947973], "Мозаика", 20), + # Point([56.269486, 43.9238], "Мозаика", 20), + Point([56.299251, 43.985146], "Мозаика", 20), + Point([56.293297, 44.034095], "Мозаика", 20), + Point([56.299251, 43.985146], "Мозаика", 20), + Point([56.229652, 43.947973], "Мозаика", 20), + Point([56.269486, 43.9238], "Мозаика", 20), + #Point([56.293297, 44.034095], "Мозаика", 20), + #Point([56.229652, 43.947973], "Мозаика", 20) + ] + + tag_importance_test = { + "Памятник": 1, + "Парк": 1, + "Мозаика": 1, + "Архитектура": 1, + #"Музей": 1 + } + + # Check tags constraint + if not check_tags_constraint(points): + print("Error: More than 5 unique tags in the input data") + return + + print("Input data validation: OK (5 or fewer unique tags)") + + # Step 1: Filter points using straight-line distance and total time + filtered_by_time = filter_points_by_time(start_coord, points, total_time) + print(f"After initial time filtering: {len(filtered_by_time)} points") + + if len(filtered_by_time) < 3: + print("Not enough points after time filtering") + return + + # Step 2: Filter points by tag proximity (keep max 10 closest points per tag) + filtered_points = filter_points_by_tag_proximity(filtered_by_time, max_per_tag=10) + print(f"After tag proximity filtering: {len(filtered_points)} points") + + if len(filtered_points) < 3: + print("Not enough points after tag proximity filtering") + return + + # Step 3: Prepare points for server request (start point + filtered points) + points_for_matrix = [start_coord] + [point.coord for point in filtered_points] + + print("Requesting duration matrix from server...") + # Step 4: Get duration matrix from server + result = get_duration_matrix(points_for_matrix) + if result is None: + print("Failed to get duration matrix from server") + return + + distances_matrix, durations_matrix = result + print("Duration matrix received successfully") + + # Assign matrix indices to points + for i, point in enumerate(filtered_points): + point.matrix_index = i + 1 # +1 because index 0 is the start point + + # Step 5: Group by importance + grouped_points = group_points_by_significance(filtered_points, tag_importance) + + # Get all unique tags + all_tags = set(point.tag for point in filtered_points) + num_unique_tags = len(all_tags) + + print(f"Unique tags: {all_tags} ({num_unique_tags} tags)") + + # Step 6: Generate possible routes + print("Generating possible routes...") + + # Determine the number of points in the route + if num_unique_tags >= n_nodes: + # Each tag must be visited exactly once + print("Each tag will be visited exactly once with unique coordinates") + possible_routes = generate_routes_exact_tags(grouped_points, all_tags, tag_importance) + else: + # We have fewer than 3 unique tags, need to repeat some tags + print(f"Only {num_unique_tags} unique tags available, will repeat tags to reach 3 points with unique coordinates") + possible_routes = generate_routes_with_repeats(grouped_points, all_tags, tag_importance, n_nodes) + + if not possible_routes: + print("No valid routes found that cover all tags with unique coordinates") + return + + # Step 7: Calculate time for each route and filter by total_time + valid_routes = [] + for route in possible_routes: + route_time = calculate_route_time_with_matrix(route, start_coord, durations_matrix) + if route_time <= total_time: + valid_routes.append((route, route_time)) + + if not valid_routes: + print("No valid routes found within time constraint") + return + + # Step 8: Find optimal route (minimum time) + optimal_route, min_time = min(valid_routes, key=lambda x: x[1]) + + print(f"\nOptimal route (time: {min_time:.2f} min):") + for i, point in enumerate(optimal_route, 1): + print(f"{i}. {point.tag} at {point.coord} ({point.visit_time} min)") + + # Print route details with travel times + print("\nRoute details:") + current_index = 0 + total_route_time = 0 + for i, point in enumerate(optimal_route): + travel_time_seconds = durations_matrix[current_index][point.matrix_index] + travel_time_minutes = travel_time_seconds / 60.0 + segment_time = travel_time_minutes + point.visit_time + total_route_time += segment_time + + print(f"Segment {i+1}: {travel_time_minutes:.2f} min travel + {point.visit_time} min visit = {segment_time:.2f} min") + current_index = point.matrix_index + + print(f"Total route time: {total_route_time:.2f} min") + + # Display all tags covered by the route + route_tags = set(point.tag for point in optimal_route) + print(f"\nTags covered in this route: {', '.join(route_tags)}") + if all_tags.issubset(route_tags): + print("All tags are covered in this route!") + + # Verify all coordinates are unique + route_coords = [tuple(point.coord) for point in optimal_route] + if len(route_coords) == len(set(route_coords)): + print("All coordinates in the route are unique!") + else: + print("ERROR: Duplicate coordinates found in the route!") + +#if __name__ == "__main__": + # build_route() \ No newline at end of file diff --git a/wrapper.py b/wrapper.py new file mode 100644 index 0000000..349cde7 --- /dev/null +++ b/wrapper.py @@ -0,0 +1,26 @@ +from database import search_database +from geocoder import validate_address +from new_parser_test import parse +from route import build_route +import json + +if __name__=='__main__': + user_input = parse() + user_input = json.loads(user_input) + print(user_input) + query =user_input['tags'] + user_address =user_input['user_location'] + user_time =user_input['time'] + val_output = validate_address('addresses.sqlite',user_address) + print(val_output) + found_points,mapping = search_database('output.json', query) + print(len(found_points)) + print(mapping) + user_position =[] + if val_output['valid']: + user_position.append(val_output['coordinates']['lat']) + user_position.append(val_output['coordinates']['lon']) + else: + print('Адрес не найден') + route = build_route(found_points, mapping,user_position,user_time,5) + print(route)