CabLate commited on
Commit
36f2866
·
1 Parent(s): 5bd9932

feat: add new tools

Browse files
README.md CHANGED
@@ -6,9 +6,24 @@ A powerful Model Context Protocol (MCP) server providing comprehensive Google Ma
6
 
7
  ### Google Maps Features
8
 
9
- - Location search and information retrieval
10
- - Geocoding and reverse geocoding
11
- - Detailed place information
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  ## Installation
14
 
@@ -48,6 +63,18 @@ mcp-google-map
48
 
49
  3. Click "Save" to complete the installation
50
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ## Google Maps API Setup
52
 
53
  To use this service, you need to:
 
6
 
7
  ### Google Maps Features
8
 
9
+ - **Location Search**
10
+
11
+ - Search for places near a specific location with customizable radius and filters
12
+ - Get detailed place information including ratings, opening hours, and contact details
13
+
14
+ - **Geocoding Services**
15
+
16
+ - Convert addresses to coordinates (geocoding)
17
+ - Convert coordinates to addresses (reverse geocoding)
18
+
19
+ - **Distance & Directions**
20
+
21
+ - Calculate distances and travel times between multiple origins and destinations
22
+ - Get detailed directions between two points with step-by-step instructions
23
+ - Support for different travel modes (driving, walking, bicycling, transit)
24
+
25
+ - **Elevation Data**
26
+ - Retrieve elevation data for specific locations
27
 
28
  ## Installation
29
 
 
63
 
64
  3. Click "Save" to complete the installation
65
 
66
+ ## Available Tools
67
+
68
+ The server provides the following tools:
69
+
70
+ 1. **search_nearby** - Search for places near a specific location
71
+ 2. **get_place_details** - Get detailed information about a specific place
72
+ 3. **maps_geocode** - Convert an address to coordinates
73
+ 4. **maps_reverse_geocode** - Convert coordinates to an address
74
+ 5. **maps_distance_matrix** - Calculate distances and times between multiple origins and destinations
75
+ 6. **maps_directions** - Get directions between two points
76
+ 7. **maps_elevation** - Get elevation data for specific locations
77
+
78
  ## Google Maps API Setup
79
 
80
  To use this service, you need to:
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "@cablate/mcp-google-map",
3
- "version": "0.0.1",
4
  "type": "module",
5
  "scripts": {
6
  "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --external:pdfreader --external:jsdom --external:mammoth --external:csv-parse --external:libreoffice-convert && shx chmod +x dist/index.cjs",
 
1
  {
2
  "name": "@cablate/mcp-google-map",
3
+ "version": "0.0.2",
4
  "type": "module",
5
  "scripts": {
6
  "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --external:pdfreader --external:jsdom --external:mammoth --external:csv-parse --external:libreoffice-convert && shx chmod +x dist/index.cjs",
src/index.ts CHANGED
@@ -3,10 +3,10 @@
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
- import { GET_PLACE_DETAILS_TOOL, SEARCH_NEARBY_TOOL } from "./maps-tools/mapsTools.js";
7
  import { PlacesSearcher } from "./maps-tools/searchPlaces.js";
8
 
9
- const tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL];
10
  const placesSearcher = new PlacesSearcher();
11
 
12
  const server = new Server(
@@ -94,6 +94,136 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
94
  };
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  return {
98
  content: [{ type: "text", text: `錯誤:未知的工具 ${name}` }],
99
  isError: true,
 
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
+ import { DIRECTIONS_TOOL, DISTANCE_MATRIX_TOOL, ELEVATION_TOOL, GEOCODE_TOOL, GET_PLACE_DETAILS_TOOL, REVERSE_GEOCODE_TOOL, SEARCH_NEARBY_TOOL } from "./maps-tools/mapsTools.js";
7
  import { PlacesSearcher } from "./maps-tools/searchPlaces.js";
8
 
9
+ const tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL, GEOCODE_TOOL, REVERSE_GEOCODE_TOOL, DISTANCE_MATRIX_TOOL, DIRECTIONS_TOOL, ELEVATION_TOOL];
10
  const placesSearcher = new PlacesSearcher();
11
 
12
  const server = new Server(
 
94
  };
95
  }
96
 
97
+ if (name === "maps_geocode") {
98
+ const { address } = args as {
99
+ address: string;
100
+ };
101
+
102
+ const result = await placesSearcher.geocode(address);
103
+
104
+ if (!result.success) {
105
+ return {
106
+ content: [{ type: "text", text: result.error || "地址轉換座標失敗" }],
107
+ isError: true,
108
+ };
109
+ }
110
+
111
+ return {
112
+ content: [
113
+ {
114
+ type: "text",
115
+ text: JSON.stringify(result.data, null, 2),
116
+ },
117
+ ],
118
+ isError: false,
119
+ };
120
+ }
121
+
122
+ if (name === "maps_reverse_geocode") {
123
+ const { latitude, longitude } = args as {
124
+ latitude: number;
125
+ longitude: number;
126
+ };
127
+
128
+ const result = await placesSearcher.reverseGeocode(latitude, longitude);
129
+
130
+ if (!result.success) {
131
+ return {
132
+ content: [{ type: "text", text: result.error || "座標轉換地址失敗" }],
133
+ isError: true,
134
+ };
135
+ }
136
+
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify(result.data, null, 2),
142
+ },
143
+ ],
144
+ isError: false,
145
+ };
146
+ }
147
+
148
+ if (name === "maps_distance_matrix") {
149
+ const { origins, destinations, mode } = args as {
150
+ origins: string[];
151
+ destinations: string[];
152
+ mode?: "driving" | "walking" | "bicycling" | "transit";
153
+ };
154
+
155
+ const result = await placesSearcher.calculateDistanceMatrix(origins, destinations, mode || "driving");
156
+
157
+ if (!result.success) {
158
+ return {
159
+ content: [{ type: "text", text: result.error || "計算距離矩陣失敗" }],
160
+ isError: true,
161
+ };
162
+ }
163
+
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify(result.data, null, 2),
169
+ },
170
+ ],
171
+ isError: false,
172
+ };
173
+ }
174
+
175
+ if (name === "maps_directions") {
176
+ const { origin, destination, mode } = args as {
177
+ origin: string;
178
+ destination: string;
179
+ mode?: "driving" | "walking" | "bicycling" | "transit";
180
+ };
181
+
182
+ const result = await placesSearcher.getDirections(origin, destination, mode || "driving");
183
+
184
+ if (!result.success) {
185
+ return {
186
+ content: [{ type: "text", text: result.error || "獲取路線指引失敗" }],
187
+ isError: true,
188
+ };
189
+ }
190
+
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: JSON.stringify(result.data, null, 2),
196
+ },
197
+ ],
198
+ isError: false,
199
+ };
200
+ }
201
+
202
+ if (name === "maps_elevation") {
203
+ const { locations } = args as {
204
+ locations: Array<{ latitude: number; longitude: number }>;
205
+ };
206
+
207
+ const result = await placesSearcher.getElevation(locations);
208
+
209
+ if (!result.success) {
210
+ return {
211
+ content: [{ type: "text", text: result.error || "獲取海拔數據失敗" }],
212
+ isError: true,
213
+ };
214
+ }
215
+
216
+ return {
217
+ content: [
218
+ {
219
+ type: "text",
220
+ text: JSON.stringify(result.data, null, 2),
221
+ },
222
+ ],
223
+ isError: false,
224
+ };
225
+ }
226
+
227
  return {
228
  content: [{ type: "text", text: `錯誤:未知的工具 ${name}` }],
229
  isError: true,
src/maps-tools/mapsTools.ts CHANGED
@@ -8,7 +8,7 @@ export const SEARCH_NEARBY_TOOL = {
8
  type: "object",
9
  properties: {
10
  value: { type: "string", description: "地址、地標名稱或經緯度座標(經緯度座標格式: lat,lng)" },
11
- isCoordinates: { type: "boolean", description: "是否為經緯度座標", default: false }
12
  },
13
  required: ["value"],
14
  description: "搜尋中心點",
@@ -38,6 +38,125 @@ export const SEARCH_NEARBY_TOOL = {
38
  },
39
  };
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  export const GET_PLACE_DETAILS_TOOL = {
42
  name: "get_place_details",
43
  description: "獲取特定地點的詳細資訊",
 
8
  type: "object",
9
  properties: {
10
  value: { type: "string", description: "地址、地標名稱或經緯度座標(經緯度座標格式: lat,lng)" },
11
+ isCoordinates: { type: "boolean", description: "是否為經緯度座標", default: false },
12
  },
13
  required: ["value"],
14
  description: "搜尋中心點",
 
38
  },
39
  };
40
 
41
+ export const GEOCODE_TOOL = {
42
+ name: "maps_geocode",
43
+ description: "將地址轉換為座標",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ address: {
48
+ type: "string",
49
+ description: "要轉換的地址或地標名稱",
50
+ },
51
+ },
52
+ required: ["address"],
53
+ },
54
+ };
55
+
56
+ export const REVERSE_GEOCODE_TOOL = {
57
+ name: "maps_reverse_geocode",
58
+ description: "將座標轉換為地址",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ latitude: {
63
+ type: "number",
64
+ description: "緯度",
65
+ },
66
+ longitude: {
67
+ type: "number",
68
+ description: "經度",
69
+ },
70
+ },
71
+ required: ["latitude", "longitude"],
72
+ },
73
+ };
74
+
75
+ export const DISTANCE_MATRIX_TOOL = {
76
+ name: "maps_distance_matrix",
77
+ description: "計算多個起點和終點之間的距離和時間",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ origins: {
82
+ type: "array",
83
+ items: {
84
+ type: "string",
85
+ },
86
+ description: "起點地址或座標列表",
87
+ },
88
+ destinations: {
89
+ type: "array",
90
+ items: {
91
+ type: "string",
92
+ },
93
+ description: "終點地址或座標列表",
94
+ },
95
+ mode: {
96
+ type: "string",
97
+ enum: ["driving", "walking", "bicycling", "transit"],
98
+ description: "交通模式",
99
+ default: "driving",
100
+ },
101
+ },
102
+ required: ["origins", "destinations"],
103
+ },
104
+ };
105
+
106
+ export const DIRECTIONS_TOOL = {
107
+ name: "maps_directions",
108
+ description: "獲取兩點之間的路線指引",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ origin: {
113
+ type: "string",
114
+ description: "起點地址或座標",
115
+ },
116
+ destination: {
117
+ type: "string",
118
+ description: "終點地址或座標",
119
+ },
120
+ mode: {
121
+ type: "string",
122
+ enum: ["driving", "walking", "bicycling", "transit"],
123
+ description: "交通模式",
124
+ default: "driving",
125
+ },
126
+ },
127
+ required: ["origin", "destination"],
128
+ },
129
+ };
130
+
131
+ export const ELEVATION_TOOL = {
132
+ name: "maps_elevation",
133
+ description: "獲取位置的海拔數據",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ locations: {
138
+ type: "array",
139
+ items: {
140
+ type: "object",
141
+ properties: {
142
+ latitude: {
143
+ type: "number",
144
+ description: "緯度",
145
+ },
146
+ longitude: {
147
+ type: "number",
148
+ description: "經度",
149
+ },
150
+ },
151
+ required: ["latitude", "longitude"],
152
+ },
153
+ description: "要獲取海拔數據的位置列表",
154
+ },
155
+ },
156
+ required: ["locations"],
157
+ },
158
+ };
159
+
160
  export const GET_PLACE_DETAILS_TOOL = {
161
  name: "get_place_details",
162
  description: "獲取特定地點的詳細資訊",
src/maps-tools/searchPlaces.ts CHANGED
@@ -13,6 +13,57 @@ interface PlaceDetailsResponse {
13
  data?: any;
14
  }
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  export class PlacesSearcher {
17
  private mapsTools: GoogleMapsTools;
18
 
@@ -84,4 +135,84 @@ export class PlacesSearcher {
84
  };
85
  }
86
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
 
13
  data?: any;
14
  }
15
 
16
+ interface GeocodeResponse {
17
+ success: boolean;
18
+ error?: string;
19
+ data?: {
20
+ location: { lat: number; lng: number };
21
+ formatted_address: string;
22
+ place_id: string;
23
+ };
24
+ }
25
+
26
+ interface ReverseGeocodeResponse {
27
+ success: boolean;
28
+ error?: string;
29
+ data?: {
30
+ formatted_address: string;
31
+ place_id: string;
32
+ address_components: any[];
33
+ };
34
+ }
35
+
36
+ interface DistanceMatrixResponse {
37
+ success: boolean;
38
+ error?: string;
39
+ data?: {
40
+ distances: any[][];
41
+ durations: any[][];
42
+ origin_addresses: string[];
43
+ destination_addresses: string[];
44
+ };
45
+ }
46
+
47
+ interface DirectionsResponse {
48
+ success: boolean;
49
+ error?: string;
50
+ data?: {
51
+ routes: any[];
52
+ summary: string;
53
+ total_distance: { value: number; text: string };
54
+ total_duration: { value: number; text: string };
55
+ };
56
+ }
57
+
58
+ interface ElevationResponse {
59
+ success: boolean;
60
+ error?: string;
61
+ data?: Array<{
62
+ elevation: number;
63
+ location: { lat: number; lng: number };
64
+ }>;
65
+ }
66
+
67
  export class PlacesSearcher {
68
  private mapsTools: GoogleMapsTools;
69
 
 
135
  };
136
  }
137
  }
138
+
139
+ async geocode(address: string): Promise<GeocodeResponse> {
140
+ try {
141
+ const result = await this.mapsTools.geocode(address);
142
+
143
+ return {
144
+ success: true,
145
+ data: result,
146
+ };
147
+ } catch (error) {
148
+ return {
149
+ success: false,
150
+ error: error instanceof Error ? error.message : "地址轉換座標時發生錯誤",
151
+ };
152
+ }
153
+ }
154
+
155
+ async reverseGeocode(latitude: number, longitude: number): Promise<ReverseGeocodeResponse> {
156
+ try {
157
+ const result = await this.mapsTools.reverseGeocode(latitude, longitude);
158
+
159
+ return {
160
+ success: true,
161
+ data: result,
162
+ };
163
+ } catch (error) {
164
+ return {
165
+ success: false,
166
+ error: error instanceof Error ? error.message : "座標轉換地址時發生錯誤",
167
+ };
168
+ }
169
+ }
170
+
171
+ async calculateDistanceMatrix(origins: string[], destinations: string[], mode: "driving" | "walking" | "bicycling" | "transit" = "driving"): Promise<DistanceMatrixResponse> {
172
+ try {
173
+ const result = await this.mapsTools.calculateDistanceMatrix(origins, destinations, mode);
174
+
175
+ return {
176
+ success: true,
177
+ data: result,
178
+ };
179
+ } catch (error) {
180
+ return {
181
+ success: false,
182
+ error: error instanceof Error ? error.message : "計算距離矩陣時發生錯誤",
183
+ };
184
+ }
185
+ }
186
+
187
+ async getDirections(origin: string, destination: string, mode: "driving" | "walking" | "bicycling" | "transit" = "driving"): Promise<DirectionsResponse> {
188
+ try {
189
+ const result = await this.mapsTools.getDirections(origin, destination, mode);
190
+
191
+ return {
192
+ success: true,
193
+ data: result,
194
+ };
195
+ } catch (error) {
196
+ return {
197
+ success: false,
198
+ error: error instanceof Error ? error.message : "獲取路線指引時發生錯誤",
199
+ };
200
+ }
201
+ }
202
+
203
+ async getElevation(locations: Array<{ latitude: number; longitude: number }>): Promise<ElevationResponse> {
204
+ try {
205
+ const result = await this.mapsTools.getElevation(locations);
206
+
207
+ return {
208
+ success: true,
209
+ data: result,
210
+ };
211
+ } catch (error) {
212
+ return {
213
+ success: false,
214
+ error: error instanceof Error ? error.message : "獲取海拔數據時發生錯誤",
215
+ };
216
+ }
217
+ }
218
  }
src/maps-tools/toolclass.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Client, Language } from "@googlemaps/google-maps-services-js";
2
  import dotenv from "dotenv";
3
 
4
  // 確保環境變數被載入
@@ -32,6 +32,8 @@ interface PlaceResult {
32
  interface GeocodeResult {
33
  lat: number;
34
  lng: number;
 
 
35
  }
36
 
37
  export class GoogleMapsTools {
@@ -97,18 +99,21 @@ export class GoogleMapsTools {
97
  params: {
98
  address: address,
99
  key: process.env.GOOGLE_MAPS_API_KEY || "",
100
- language: this.defaultLanguage
101
- }
102
  });
103
 
104
  if (response.data.results.length === 0) {
105
  throw new Error("找不到該地址的位置");
106
  }
107
 
108
- const location = response.data.results[0].geometry.location;
 
109
  return {
110
  lat: location.lat,
111
- lng: location.lng
 
 
112
  };
113
  } catch (error) {
114
  console.error("Error in geocodeAddress:", error);
@@ -117,7 +122,7 @@ export class GoogleMapsTools {
117
  }
118
 
119
  private parseCoordinates(coordString: string): GeocodeResult {
120
- const coords = coordString.split(',').map(c => parseFloat(c.trim()));
121
  if (coords.length !== 2 || isNaN(coords[0]) || isNaN(coords[1])) {
122
  throw new Error("無效的座標格式,請使用「緯度,經度」格式");
123
  }
@@ -130,4 +135,204 @@ export class GoogleMapsTools {
130
  }
131
  return this.geocodeAddress(center.value);
132
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
 
1
+ import { Client, Language, TravelMode } from "@googlemaps/google-maps-services-js";
2
  import dotenv from "dotenv";
3
 
4
  // 確保環境變數被載入
 
32
  interface GeocodeResult {
33
  lat: number;
34
  lng: number;
35
+ formatted_address?: string;
36
+ place_id?: string;
37
  }
38
 
39
  export class GoogleMapsTools {
 
99
  params: {
100
  address: address,
101
  key: process.env.GOOGLE_MAPS_API_KEY || "",
102
+ language: this.defaultLanguage,
103
+ },
104
  });
105
 
106
  if (response.data.results.length === 0) {
107
  throw new Error("找不到該地址的位置");
108
  }
109
 
110
+ const result = response.data.results[0];
111
+ const location = result.geometry.location;
112
  return {
113
  lat: location.lat,
114
+ lng: location.lng,
115
+ formatted_address: result.formatted_address,
116
+ place_id: result.place_id,
117
  };
118
  } catch (error) {
119
  console.error("Error in geocodeAddress:", error);
 
122
  }
123
 
124
  private parseCoordinates(coordString: string): GeocodeResult {
125
+ const coords = coordString.split(",").map((c) => parseFloat(c.trim()));
126
  if (coords.length !== 2 || isNaN(coords[0]) || isNaN(coords[1])) {
127
  throw new Error("無效的座標格式,請使用「緯度,經度」格式");
128
  }
 
135
  }
136
  return this.geocodeAddress(center.value);
137
  }
138
+
139
+ // 新增公開方法用於地址轉座標
140
+ async geocode(address: string): Promise<{
141
+ location: { lat: number; lng: number };
142
+ formatted_address: string;
143
+ place_id: string;
144
+ }> {
145
+ try {
146
+ const result = await this.geocodeAddress(address);
147
+ return {
148
+ location: { lat: result.lat, lng: result.lng },
149
+ formatted_address: result.formatted_address || "",
150
+ place_id: result.place_id || "",
151
+ };
152
+ } catch (error) {
153
+ console.error("Error in geocode:", error);
154
+ throw new Error("地址轉換座標時發生錯誤");
155
+ }
156
+ }
157
+
158
+ async reverseGeocode(
159
+ latitude: number,
160
+ longitude: number
161
+ ): Promise<{
162
+ formatted_address: string;
163
+ place_id: string;
164
+ address_components: any[];
165
+ }> {
166
+ try {
167
+ const response = await this.client.reverseGeocode({
168
+ params: {
169
+ latlng: { lat: latitude, lng: longitude },
170
+ language: this.defaultLanguage,
171
+ key: process.env.GOOGLE_MAPS_API_KEY || "",
172
+ },
173
+ });
174
+
175
+ if (response.data.results.length === 0) {
176
+ throw new Error("找不到該座標的地址");
177
+ }
178
+
179
+ const result = response.data.results[0];
180
+ return {
181
+ formatted_address: result.formatted_address,
182
+ place_id: result.place_id,
183
+ address_components: result.address_components,
184
+ };
185
+ } catch (error) {
186
+ console.error("Error in reverseGeocode:", error);
187
+ throw new Error("座標轉換地址時發生錯誤");
188
+ }
189
+ }
190
+
191
+ async calculateDistanceMatrix(
192
+ origins: string[],
193
+ destinations: string[],
194
+ mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
195
+ ): Promise<{
196
+ distances: any[][];
197
+ durations: any[][];
198
+ origin_addresses: string[];
199
+ destination_addresses: string[];
200
+ }> {
201
+ try {
202
+ const response = await this.client.distancematrix({
203
+ params: {
204
+ origins: origins,
205
+ destinations: destinations,
206
+ mode: mode as TravelMode,
207
+ language: this.defaultLanguage,
208
+ key: process.env.GOOGLE_MAPS_API_KEY || "",
209
+ },
210
+ });
211
+
212
+ const result = response.data;
213
+
214
+ if (result.status !== "OK") {
215
+ throw new Error(`距離矩陣計算失敗: ${result.status}`);
216
+ }
217
+
218
+ const distances: any[][] = [];
219
+ const durations: any[][] = [];
220
+
221
+ result.rows.forEach((row: any) => {
222
+ const distanceRow: any[] = [];
223
+ const durationRow: any[] = [];
224
+
225
+ row.elements.forEach((element: any) => {
226
+ if (element.status === "OK") {
227
+ distanceRow.push({
228
+ value: element.distance.value,
229
+ text: element.distance.text,
230
+ });
231
+ durationRow.push({
232
+ value: element.duration.value,
233
+ text: element.duration.text,
234
+ });
235
+ } else {
236
+ distanceRow.push(null);
237
+ durationRow.push(null);
238
+ }
239
+ });
240
+
241
+ distances.push(distanceRow);
242
+ durations.push(durationRow);
243
+ });
244
+
245
+ return {
246
+ distances: distances,
247
+ durations: durations,
248
+ origin_addresses: result.origin_addresses,
249
+ destination_addresses: result.destination_addresses,
250
+ };
251
+ } catch (error) {
252
+ console.error("Error in calculateDistanceMatrix:", error);
253
+ throw new Error("計算距離矩陣時發生錯誤");
254
+ }
255
+ }
256
+
257
+ async getDirections(
258
+ origin: string,
259
+ destination: string,
260
+ mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
261
+ ): Promise<{
262
+ routes: any[];
263
+ summary: string;
264
+ total_distance: { value: number; text: string };
265
+ total_duration: { value: number; text: string };
266
+ }> {
267
+ try {
268
+ const response = await this.client.directions({
269
+ params: {
270
+ origin: origin,
271
+ destination: destination,
272
+ mode: mode as TravelMode,
273
+ language: this.defaultLanguage,
274
+ key: process.env.GOOGLE_MAPS_API_KEY || "",
275
+ },
276
+ });
277
+
278
+ const result = response.data;
279
+
280
+ if (result.status !== "OK") {
281
+ throw new Error(`路線指引獲取失敗: ${result.status}`);
282
+ }
283
+
284
+ if (result.routes.length === 0) {
285
+ throw new Error("找不到路線");
286
+ }
287
+
288
+ const route = result.routes[0];
289
+ const legs = route.legs[0];
290
+
291
+ return {
292
+ routes: result.routes,
293
+ summary: route.summary,
294
+ total_distance: {
295
+ value: legs.distance.value,
296
+ text: legs.distance.text,
297
+ },
298
+ total_duration: {
299
+ value: legs.duration.value,
300
+ text: legs.duration.text,
301
+ },
302
+ };
303
+ } catch (error) {
304
+ console.error("Error in getDirections:", error);
305
+ throw new Error("獲取路線指引時發生錯誤");
306
+ }
307
+ }
308
+
309
+ async getElevation(locations: Array<{ latitude: number; longitude: number }>): Promise<Array<{ elevation: number; location: { lat: number; lng: number } }>> {
310
+ try {
311
+ const formattedLocations = locations.map((loc) => ({
312
+ lat: loc.latitude,
313
+ lng: loc.longitude,
314
+ }));
315
+
316
+ const response = await this.client.elevation({
317
+ params: {
318
+ locations: formattedLocations,
319
+ key: process.env.GOOGLE_MAPS_API_KEY || "",
320
+ },
321
+ });
322
+
323
+ const result = response.data;
324
+
325
+ if (result.status !== "OK") {
326
+ throw new Error(`海拔數據獲取失敗: ${result.status}`);
327
+ }
328
+
329
+ return result.results.map((item: any, index: number) => ({
330
+ elevation: item.elevation,
331
+ location: formattedLocations[index],
332
+ }));
333
+ } catch (error) {
334
+ console.error("Error in getElevation:", error);
335
+ throw new Error("獲取海拔數據時發生錯誤");
336
+ }
337
+ }
338
  }