| import json |
| import time |
| import random |
|
|
| import pandas as pd |
| import numpy as np |
| import re |
|
|
| |
| extract_poi_coordinates = lambda poi_data: np.array([ |
| [poi["location"]["latitude"], poi["location"]["longitude"]] |
| for poi in poi_data if poi.get("location") and "latitude" in poi["location"] and "longitude" in poi["location"] |
| ]) |
|
|
|
|
| def filter_pois(poi_with_distances, parsed_targets): |
| """執行一系列過濾操作""" |
| |
| filtered_target_pois = filter_by_target(poi_with_distances, parsed_targets) |
| |
| filtered_distance_pois = filter_by_distance(filtered_target_pois, parsed_targets) |
| |
| filtered_open_pois = filter_by_open(filtered_distance_pois, parsed_targets) |
| |
| filtered_rating_pois = filter_or_sort_by_rating(filtered_open_pois, parsed_targets) |
| |
| top_filtered_pois = filter_today_opening_hours(filtered_rating_pois) |
| return top_filtered_pois |
|
|
|
|
| |
| def filter_and_get_top_pois(user_location, top_filtered_pois): |
| _, user_lon = user_location |
| |
| |
| east_pois = [ |
| poi for poi in top_filtered_pois |
| if poi['location']['longitude'] > user_lon |
| ] |
| |
| |
| east_pois.sort(key=lambda x: x['distance']) |
| |
| |
| return east_pois[:3] |
|
|
|
|
| def filter_poi_list(data_list): |
|
|
| needed_keys = [ |
| "englishName", |
| "types", |
| "primaryType", |
| "rating", |
| "location", |
| "formattedAddress", |
| "userRatingCount", |
| "editorialSummary" |
| ] |
| |
| filtered = [] |
| for poi in data_list: |
|
|
| filtered_dict = {key: poi.get(key) for key in needed_keys} |
| regular_hours = poi.get("regularOpeningHours") |
| if regular_hours and "periods" in regular_hours: |
| filtered_dict["regularOpeningHours"] = { |
| "periods": regular_hours["periods"] |
| } |
| else: |
| |
| filtered_dict["regularOpeningHours"] = None |
| |
| filtered.append(filtered_dict) |
| |
| return filtered |
|
|
|
|
|
|
| def get_coordinates(location_data, video_tps_value): |
| """ |
| 從 DataFrame 中檢索特定 video_tps 的經緯度座標。 |
| """ |
| matched_row = location_data[location_data['video_tps'] == video_tps_value] |
| |
| if matched_row.empty: |
| |
| return [] |
| |
| |
| latitude = matched_row['latitude'].values[0] |
| longitude = matched_row['longitude'].values[0] |
| |
| return (latitude, longitude) |
|
|
|
|
|
|
|
|
| |
| def calculate_distances(point, points_array): |
| lat1, lon1 = point |
| lat2 = points_array[:, 0] |
| lon2 = points_array[:, 1] |
| |
| |
| lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) |
| |
| |
| dlon = lon2 - lon1 |
| dlat = lat2 - lat1 |
| |
| |
| a = np.sin(dlat / 2.0) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2.0) ** 2 |
| c = 2 * np.arcsin(np.sqrt(a)) |
| |
| r = 6371000 |
| distances_in_meters = c * r |
| |
| |
| return np.round(distances_in_meters, 2) |
|
|
|
|
|
|
| def add_distances_to_poi(poi_data, distances): |
| |
| poi_with_distances = [] |
| for i, poi in enumerate(poi_data): |
| |
| poi_with_distance = poi.copy() |
| poi_with_distance['distance'] = distances[i] |
| poi_with_distances.append(poi_with_distance) |
|
|
| return poi_with_distances |
|
|
|
|
|
|
| def parse_ner_targets(parsed_entities): |
| """ |
| 將 parsed_entities 中的 target 拆解為 primarytype 和 displayname, |
| """ |
| |
| primary_categories = {"restaurant", "shopping_mall", "hospital", "parking", "tourist_attraction", |
| "convenience_store", "bank", "car_repair", "electric_vehicle_charging_station", |
| "gas_station", "movie_theater"} |
| |
| parsed_result = { |
| "primarytype": '', |
| "displayname": '', |
| "distance": parsed_entities.get("distance", []), |
| "open": parsed_entities.get("open", []), |
| "rank": parsed_entities.get("rank", []) |
| } |
| targets = parsed_entities.get("target", []) |
|
|
| for target in targets: |
| if target in primary_categories: |
| parsed_result["primarytype"] = target |
| else: |
| parsed_result["displayname"] = target |
| return parsed_result |
|
|
|
|
|
|
| def filter_by_target(poi_with_distances, parsed_targets): |
| """ |
| 用 parsed_targets 的內容篩選 poi_with_distances。 |
| |
| 1. 如果 parsed_targets['displayname'] 不為空字串 (''), |
| 以正則匹配完整詞,不被其他字母數字相鄰 |
| 2. 若 parsed_targets['displayname'] 為空字串 (''), |
| 則以 parsed_targets['primarytype'] 檢查 poi_with_distances['types'] 中是否包含 |
| """ |
| target_displayname = parsed_targets.get("displayname", "").strip() |
| target_type = parsed_targets.get("primarytype", "").strip() |
|
|
| if target_displayname: |
| print('target_displayname') |
| pattern = re.compile(rf'\b{re.escape(target_displayname.lower())}\b') |
| filtered_pois = [ |
| poi for poi in poi_with_distances |
| if pattern.search(poi['englishName'].lower()) |
| ] |
| else: |
| |
| filtered_pois = [ |
| poi for poi in poi_with_distances |
| if target_type in poi.get("types", []) |
| ] |
| |
| return filtered_pois |
|
|
|
|
|
|
| |
| def filter_by_distance(poi_list, parsed_targets): |
| """ |
| 1) 先按照 distance 升冪(由近到遠)進行排序 |
| 2) 若 parsed_targets['distance'] = ['500'],則只保留 distance < 500 的項目 |
| 若無 distance 或 distance 為空,則返回原清單(但也還是已經排序好) |
| """ |
| |
| sorted_by_distance = sorted( |
| poi_list, |
| key=lambda x: x.get("distance", float("inf")) |
| ) |
| |
| |
| if not parsed_targets.get("distance"): |
| return sorted_by_distance |
| |
| |
| distance_str = parsed_targets["distance"][0] |
| distance_threshold = float(distance_str) |
| |
| |
| filtered_result = [ |
| poi for poi in sorted_by_distance |
| if poi.get("distance") is not None and poi["distance"] < distance_threshold |
| ] |
| |
| return filtered_result |
|
|
|
|
|
|
| |
| def filter_or_sort_by_rating(poi_list, parsed_targets): |
| """ |
| 若 parsed_targets['rank'] 為小數點字串 (例如 "4.0") => 過濾 rating >= 該值 |
| 若 parsed_targets['rank'] 為 "highest" => 依 rating 高到低排序 |
| 其他情況 (例如沒有 rank 或 rank 為空) 則不處理,直接返回原清單 |
| """ |
| |
| if not parsed_targets.get("rank"): |
| return poi_list |
| |
| rank_value = parsed_targets["rank"][0] |
| |
| |
| if rank_value.lower() == "highest": |
| return sorted(poi_list, key=lambda x: x["rating"] if x["rating"] is not None else 0, reverse=True) |
| |
| |
| rank_float = float(rank_value) |
| return [ |
| poi for poi in poi_list |
| if poi.get("rating") and poi["rating"] >= rank_float |
| ] |
|
|
|
|
|
|
|
|
| |
| def filter_by_open(poi_with_distances, parsed_targets): |
| """ |
| 不動原始 poi_with_distances,只回傳一個「新清單」。 |
| 在新清單的每個 POI dict 中,新增/更新 key: "isOpenNow" (True/False), |
| 表示「在當下星期 + parsed_targets['open'] 時間下」是否營業。 |
| |
| - 若 parsed_targets['open'] 是空,則直接將 isOpenNow 設為 None (不判斷)。 |
| - 其餘欄位皆照原資料保留。 |
| """ |
| |
| if not parsed_targets.get("open"): |
| new_list = [] |
| for poi in poi_with_distances: |
| poi_copy = poi.copy() |
| poi_copy["isOpenNow"] = None |
| new_list.append(poi_copy) |
| return new_list |
| |
| |
| target_hour = float(parsed_targets["open"][0]) |
| |
| current_weekday_py = time.localtime().tm_wday |
| |
| current_weekday_poi = (current_weekday_py + 1) % 7 |
|
|
| new_list = [] |
| for poi in poi_with_distances: |
| poi_copy = poi.copy() |
| |
| poi_copy["isOpenNow"] = False |
|
|
| regular_hours = poi_copy.get("regularOpeningHours") |
| if regular_hours and "periods" in regular_hours: |
| periods = regular_hours["periods"] |
| |
| for p in periods: |
| if p["open"]["day"] == current_weekday_poi: |
| open_hour = p["open"]["hour"] |
| close_hour = p["close"]["hour"] |
| |
| if open_hour <= target_hour < close_hour: |
| poi_copy["isOpenNow"] = True |
| break |
| |
| new_list.append(poi_copy) |
| |
| return new_list |
|
|
|
|
| def filter_today_opening_hours(poi_list): |
| """ |
| 過濾 POI 列表中的營業時間,僅保留今天的時間段。 |
| """ |
| |
| today = time.localtime().tm_wday |
|
|
| |
| for poi in poi_list: |
| |
| if poi.get('regularOpeningHours') is None or 'periods' not in poi['regularOpeningHours']: |
| continue |
|
|
| |
| poi['regularOpeningHours']['periods'] = [ |
| period for period in poi['regularOpeningHours']['periods'] |
| if period['open']['day'] == today |
| ] |
| |
| return poi_list |
|
|
|
|
|
|
| def filter_pois_by_rating_count(pois, user_rating_threshold=1000): |
| filtered_pois = [ |
| poi for poi in pois |
| if (poi.get('userRatingCount') or 0) >= user_rating_threshold |
| ] |
| return filtered_pois |
|
|
|
|
| |
| def filter_pois_by_type(pois, poi_type): |
| """ |
| Filters a list of POIs based on a specified type. |
| """ |
| return [poi for poi in pois if poi_type in poi.get("types", [])] |
|
|
|
|
|
|
| def format_english_name(english_name): |
| """ |
| 接收 englishname 字串,並根據分隔符號處理字串。 |
| """ |
| |
| |
| parts = re.split(r'[||//\\\]', english_name) |
| |
| |
| if len(parts) > 1: |
| main_part = parts[0].strip() |
| first_segment = parts[1].strip() |
| return f"{main_part} ({first_segment})" |
| |
| return english_name |
|
|
|
|
| def truncate_and_fix_brackets(input_str, max_length=70): |
| """ |
| 簡化並修正括號的字串處理 |
| 1. 若字串超過指定長度(預設 70 字),則確保不在單字中間截斷,並適當延伸到完整單字結束後。 |
| 2. 截斷後,若發現未完整的括號(如 `(` `(` `{` `[`),則自動補齊對應的右括號。 |
| """ |
| |
| if len(input_str) <= max_length: |
| return input_str |
|
|
| |
| truncated_str = input_str[:max_length] |
| remaining_str = input_str[max_length:] |
|
|
| |
| match = re.search(r'[\s,!)\]\},)]', remaining_str) |
| if match: |
| truncated_str += remaining_str[:match.start()] |
| else: |
| truncated_str = input_str |
|
|
| |
| bracket_pairs = {'(': ')', '(': ')', '[': ']', '{': '}'} |
| open_brackets = [] |
|
|
| for char in truncated_str: |
| if char in bracket_pairs: |
| open_brackets.append(bracket_pairs[char]) |
| elif char in bracket_pairs.values(): |
| if open_brackets and open_brackets[-1] == char: |
| open_brackets.pop() |
|
|
| |
| for missing in reversed(open_brackets): |
| truncated_str += missing |
|
|
| return truncated_str |
|
|
|
|
| def generate_tts_message(top_filtered_pois): |
| """ |
| 生成禮貌 自然語氣的TTS。輸出找到的店名,並隨機選擇一種禮貌模板。 |
| """ |
| num_places = len(top_filtered_pois) |
| if num_places == 0: |
| return "I'm sorry, I couldn't find any matching places nearby. Please try adjusting your preferences." |
| |
| |
| templates = [ |
| "I found {num_places} places that might interest you nearby: {details} Have a wonderful day ahead!", |
| "Here are {num_places} nearby places you might like: {details} Hope this helps you!", |
| "I've identified {num_places} places nearby: {details} Wishing you a pleasant journey!", |
| "There are {num_places} places nearby that you might enjoy: {details} Have a great time!", |
| "Nearby, I found {num_places} places worth checking out: {details} Enjoy exploring!" |
| ] |
| |
| |
| chosen_template = random.choice(templates) |
|
|
| |
| details = ", ".join( |
| [f"***{format_english_name(truncate_and_fix_brackets(poi.get('englishName', 'Unknown Place')))}***" for poi in top_filtered_pois] |
| ) + "." |
|
|
| |
| final_message = chosen_template.format(num_places=num_places, details=details) |
| return final_message |
|
|
|
|
|
|
|
|
|
|
| def generate_display_message(top_filtered_pois): |
| """ |
| 生成自然語言的顯示消息,包含店名、評分、距離(整數公尺)、與英文簡介。 |
| 使用多個模板,隨機選擇一種生成消息。 |
| """ |
| num_places = len(top_filtered_pois) |
| if num_places == 0: |
| return "I'm sorry, I couldn't find any places that match your request in the nearby area. Please try expanding the search radius or adjusting your preferences." |
| |
| |
| templates = [ |
| "I found {num_places} places nearby. \n{details} Enjoy exploring!", |
| "Here are {num_places} nearby options: \n{details} Hope you find this helpful!", |
| "There are {num_places} places worth checking out. \n{details} Have a great time!", |
| "Nearby, I identified {num_places} options you might like. \n{details} Wishing you a pleasant experience!", |
| "I discovered {num_places} places close by. \n{details} Enjoy your visit!" |
| ] |
|
|
| |
| chosen_template = random.choice(templates) |
|
|
| |
| details = "\n".join([ |
| f"{idx}. {format_english_name(truncate_and_fix_brackets(poi.get('englishName', 'Unknown Place')))}:\n" |
| f" - Rating: {poi.get('rating', 'No rating')}/5\n" |
| f" - Distance: {int(poi.get('distance', 0))} meters\n" |
| f" - Summary: {poi.get('editorialSummary', {}).get('englishSummary', 'No summary available.') if isinstance(poi.get('editorialSummary'), dict) else 'No summary available.'}" |
| for idx, poi in enumerate(top_filtered_pois, start=1) |
| ]) |
|
|
| |
| final_message = chosen_template.format(num_places=num_places, details=details) |
| return final_message |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|