import { Client, Language, TravelMode } from "@googlemaps/google-maps-services-js"; import dotenv from "dotenv"; // 確保環境變數被載入 dotenv.config(); interface SearchParams { location: { lat: number; lng: number }; radius?: number; keyword?: string; openNow?: boolean; minRating?: number; } interface PlaceResult { name: string; place_id: string; formatted_address?: string; geometry: { location: { lat: number; lng: number; }; }; rating?: number; user_ratings_total?: number; opening_hours?: { open_now?: boolean; }; } interface GeocodeResult { lat: number; lng: number; formatted_address?: string; place_id?: string; } export class GoogleMapsTools { private client: Client; private readonly defaultLanguage: Language = Language.zh_TW; constructor() { this.client = new Client({}); if (!process.env.GOOGLE_MAPS_API_KEY) { throw new Error("Google Maps API Key is required"); } } async searchNearbyPlaces(params: SearchParams): Promise { const searchParams = { location: params.location, radius: params.radius || 1000, keyword: params.keyword, opennow: params.openNow, language: this.defaultLanguage, key: process.env.GOOGLE_MAPS_API_KEY || "", }; try { const response = await this.client.placesNearby({ params: searchParams, }); let results = response.data.results; // 如果有最低評分要求,進行過濾 if (params.minRating) { results = results.filter((place) => (place.rating || 0) >= (params.minRating || 0)); } return results as PlaceResult[]; } catch (error) { console.error("Error in searchNearbyPlaces:", error); throw new Error("搜尋附近地點時發生錯誤"); } } async getPlaceDetails(placeId: string) { try { const response = await this.client.placeDetails({ params: { place_id: placeId, fields: ["name", "rating", "formatted_address", "opening_hours", "reviews", "geometry", "formatted_phone_number", "website", "price_level", "photos"], language: this.defaultLanguage, key: process.env.GOOGLE_MAPS_API_KEY || "", }, }); return response.data.result; } catch (error) { console.error("Error in getPlaceDetails:", error); throw new Error("獲取地點詳細資訊時發生錯誤"); } } private async geocodeAddress(address: string): Promise { try { const response = await this.client.geocode({ params: { address: address, key: process.env.GOOGLE_MAPS_API_KEY || "", language: this.defaultLanguage, }, }); if (response.data.results.length === 0) { throw new Error("找不到該地址的位置"); } const result = response.data.results[0]; const location = result.geometry.location; return { lat: location.lat, lng: location.lng, formatted_address: result.formatted_address, place_id: result.place_id, }; } catch (error) { console.error("Error in geocodeAddress:", error); throw new Error("地址轉換座標時發生錯誤"); } } private parseCoordinates(coordString: string): GeocodeResult { const coords = coordString.split(",").map((c) => parseFloat(c.trim())); if (coords.length !== 2 || isNaN(coords[0]) || isNaN(coords[1])) { throw new Error("無效的座標格式,請使用「緯度,經度」格式"); } return { lat: coords[0], lng: coords[1] }; } async getLocation(center: { value: string; isCoordinates: boolean }): Promise { if (center.isCoordinates) { return this.parseCoordinates(center.value); } return this.geocodeAddress(center.value); } // 新增公開方法用於地址轉座標 async geocode(address: string): Promise<{ location: { lat: number; lng: number }; formatted_address: string; place_id: string; }> { try { const result = await this.geocodeAddress(address); return { location: { lat: result.lat, lng: result.lng }, formatted_address: result.formatted_address || "", place_id: result.place_id || "", }; } catch (error) { console.error("Error in geocode:", error); throw new Error("地址轉換座標時發生錯誤"); } } async reverseGeocode( latitude: number, longitude: number ): Promise<{ formatted_address: string; place_id: string; address_components: any[]; }> { try { const response = await this.client.reverseGeocode({ params: { latlng: { lat: latitude, lng: longitude }, language: this.defaultLanguage, key: process.env.GOOGLE_MAPS_API_KEY || "", }, }); if (response.data.results.length === 0) { throw new Error("找不到該座標的地址"); } const result = response.data.results[0]; return { formatted_address: result.formatted_address, place_id: result.place_id, address_components: result.address_components, }; } catch (error) { console.error("Error in reverseGeocode:", error); throw new Error("座標轉換地址時發生錯誤"); } } async calculateDistanceMatrix( origins: string[], destinations: string[], mode: "driving" | "walking" | "bicycling" | "transit" = "driving" ): Promise<{ distances: any[][]; durations: any[][]; origin_addresses: string[]; destination_addresses: string[]; }> { try { const response = await this.client.distancematrix({ params: { origins: origins, destinations: destinations, mode: mode as TravelMode, language: this.defaultLanguage, key: process.env.GOOGLE_MAPS_API_KEY || "", }, }); const result = response.data; if (result.status !== "OK") { throw new Error(`距離矩陣計算失敗: ${result.status}`); } const distances: any[][] = []; const durations: any[][] = []; result.rows.forEach((row: any) => { const distanceRow: any[] = []; const durationRow: any[] = []; row.elements.forEach((element: any) => { if (element.status === "OK") { distanceRow.push({ value: element.distance.value, text: element.distance.text, }); durationRow.push({ value: element.duration.value, text: element.duration.text, }); } else { distanceRow.push(null); durationRow.push(null); } }); distances.push(distanceRow); durations.push(durationRow); }); return { distances: distances, durations: durations, origin_addresses: result.origin_addresses, destination_addresses: result.destination_addresses, }; } catch (error) { console.error("Error in calculateDistanceMatrix:", error); throw new Error("計算距離矩陣時發生錯誤"); } } async getDirections( origin: string, destination: string, mode: "driving" | "walking" | "bicycling" | "transit" = "driving" ): Promise<{ routes: any[]; summary: string; total_distance: { value: number; text: string }; total_duration: { value: number; text: string }; }> { try { const response = await this.client.directions({ params: { origin: origin, destination: destination, mode: mode as TravelMode, language: this.defaultLanguage, key: process.env.GOOGLE_MAPS_API_KEY || "", }, }); const result = response.data; if (result.status !== "OK") { throw new Error(`路線指引獲取失敗: ${result.status}`); } if (result.routes.length === 0) { throw new Error("找不到路線"); } const route = result.routes[0]; const legs = route.legs[0]; return { routes: result.routes, summary: route.summary, total_distance: { value: legs.distance.value, text: legs.distance.text, }, total_duration: { value: legs.duration.value, text: legs.duration.text, }, }; } catch (error) { console.error("Error in getDirections:", error); throw new Error("獲取路線指引時發生錯誤"); } } async getElevation(locations: Array<{ latitude: number; longitude: number }>): Promise> { try { const formattedLocations = locations.map((loc) => ({ lat: loc.latitude, lng: loc.longitude, })); const response = await this.client.elevation({ params: { locations: formattedLocations, key: process.env.GOOGLE_MAPS_API_KEY || "", }, }); const result = response.data; if (result.status !== "OK") { throw new Error(`海拔數據獲取失敗: ${result.status}`); } return result.results.map((item: any, index: number) => ({ elevation: item.elevation, location: formattedLocations[index], })); } catch (error) { console.error("Error in getElevation:", error); throw new Error("獲取海拔數據時發生錯誤"); } } }