Spaces:
Configuration error
Configuration error
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<PlaceResult[]> { | |
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<GeocodeResult> { | |
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<GeocodeResult> { | |
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<Array<{ elevation: number; location: { lat: number; lng: number } }>> { | |
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("獲取海拔數據時發生錯誤"); | |
} | |
} | |
} | |