@cablate/mcp-google-map 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -17
- package/dist/chunk-72XPXAUJ.js +1 -0
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/google-maps/SKILL.md +34 -18
- package/skills/google-maps/references/tools-api.md +163 -107
- package/skills/google-maps/references/travel-planning.md +140 -0
- package/skills/project-docs/SKILL.md +66 -0
- package/skills/project-docs/references/architecture.md +135 -0
- package/skills/project-docs/references/decisions.md +149 -0
- package/skills/project-docs/references/geo-domain-knowledge.md +286 -0
- package/skills/project-docs/references/google-maps-api-guide.md +139 -0
- package/dist/chunk-TP4VNBCV.js +0 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Geo Domain Knowledge for AI Map Tool Operators
|
|
2
|
+
|
|
3
|
+
> Purpose: Give an AI agent without GIS background the domain knowledge needed to use map tools correctly and confidently.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Coordinate Systems
|
|
8
|
+
|
|
9
|
+
**WGS84** is the universal standard. Every latitude/longitude pair you encounter in Google Maps APIs uses WGS84. No conversion needed.
|
|
10
|
+
|
|
11
|
+
**Order convention — lat comes first:**
|
|
12
|
+
- Correct: `(35.6762, 139.6503)` — Tokyo
|
|
13
|
+
- Wrong: `(139.6503, 35.6762)` — inverted, puts you in the ocean
|
|
14
|
+
- Google Maps API parameters are always `latitude`, `longitude` in that order.
|
|
15
|
+
|
|
16
|
+
**Precision and real-world accuracy:**
|
|
17
|
+
|
|
18
|
+
| Decimal places | Precision | Notes |
|
|
19
|
+
|----------------|-----------|-------|
|
|
20
|
+
| 0 (e.g., 35°) | ~111 km | Country-level |
|
|
21
|
+
| 1 (35.6°) | ~11 km | City-level |
|
|
22
|
+
| 2 (35.67°) | ~1.1 km | District-level |
|
|
23
|
+
| 3 (35.676°) | ~111 m | Street-level |
|
|
24
|
+
| 4 (35.6762°) | ~11 m | Building-level |
|
|
25
|
+
| 5 (35.67620°) | ~1.1 m | Door-level |
|
|
26
|
+
| 6+ | <1 m | Survey-grade, rarely needed |
|
|
27
|
+
|
|
28
|
+
For navigation and place lookup, 4–5 decimal places is sufficient. Do not truncate coordinates returned by the API — pass them as-is to downstream tools.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Distance Concepts
|
|
33
|
+
|
|
34
|
+
**Latitude degree is nearly constant worldwide:**
|
|
35
|
+
- 1° latitude ≈ 111 km everywhere
|
|
36
|
+
|
|
37
|
+
**Longitude degree varies with latitude:**
|
|
38
|
+
- At equator (0°): 1° longitude ≈ 111 km
|
|
39
|
+
- At 35°N (Tokyo/Seoul/Beijing): 1° longitude ≈ 91 km
|
|
40
|
+
- At 45°N (Paris/Milan): 1° longitude ≈ 78 km
|
|
41
|
+
- At 60°N (Oslo/Helsinki): 1° longitude ≈ 55 km
|
|
42
|
+
|
|
43
|
+
**Quick mental math:** At Tokyo's latitude, a 0.01° difference is roughly 1 km.
|
|
44
|
+
|
|
45
|
+
**Speed references for time estimation:**
|
|
46
|
+
|
|
47
|
+
| Mode | Typical speed | Notes |
|
|
48
|
+
|------|--------------|-------|
|
|
49
|
+
| Walking | ~5 km/h | 1 km = ~12 min |
|
|
50
|
+
| Cycling | ~15 km/h | varies by terrain |
|
|
51
|
+
| Urban driving | ~30–40 km/h | traffic included |
|
|
52
|
+
| Highway driving | ~80–100 km/h | intercity |
|
|
53
|
+
| Transit (urban) | ~20–30 km/h door-to-door | includes wait time |
|
|
54
|
+
| Shinkansen | ~250–300 km/h | between major cities |
|
|
55
|
+
|
|
56
|
+
These are rule-of-thumb values. Always use `maps_directions` or `maps_distance_matrix` for real travel time — actual conditions (traffic, transit schedules) differ significantly.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 3. Geocoding Concepts
|
|
61
|
+
|
|
62
|
+
**Forward geocoding**: address/name → lat/lng
|
|
63
|
+
- Input: "Shibuya Station, Tokyo"
|
|
64
|
+
- Output: `{ lat: 35.6580, lng: 139.7016, place_id: "ChIJ...", formatted_address: "..." }`
|
|
65
|
+
|
|
66
|
+
**Reverse geocoding**: lat/lng → address
|
|
67
|
+
- Input: `(35.6580, 139.7016)`
|
|
68
|
+
- Output: formatted address + place types for that location
|
|
69
|
+
- Use case: "What is at these coordinates?"
|
|
70
|
+
|
|
71
|
+
**place_id** — the stable identifier for a place in Google's database:
|
|
72
|
+
- Format: `ChIJN1t_tDeuEmsRUsoyG83frY4` (opaque string)
|
|
73
|
+
- Stable across time (unlike coordinates, which can shift if a business moves)
|
|
74
|
+
- Preferred input for `maps_place_details` — faster and more precise than re-searching by name
|
|
75
|
+
- Always cache `place_id` when you receive it; reuse in subsequent calls
|
|
76
|
+
|
|
77
|
+
**formatted_address vs raw input:**
|
|
78
|
+
- `formatted_address` is Google's canonical form: `"2 Chome-21-1 Asakusa, Taito City, Tokyo 111-0032, Japan"`
|
|
79
|
+
- Use it for display and for chaining into other tools that accept address strings
|
|
80
|
+
- Do not invent or modify addresses — only pass what Google returned
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Place Types
|
|
85
|
+
|
|
86
|
+
Google's place type system is hierarchical. A place can have multiple types (e.g., a convenience store is both `convenience_store` and `store`).
|
|
87
|
+
|
|
88
|
+
**Common type categories:**
|
|
89
|
+
|
|
90
|
+
| Category | Examples |
|
|
91
|
+
|----------|---------|
|
|
92
|
+
| Food & drink | `restaurant`, `cafe`, `bar`, `bakery`, `meal_takeaway`, `food` |
|
|
93
|
+
| Lodging | `lodging`, `hotel`, `hostel`, `guest_house` |
|
|
94
|
+
| Transport | `train_station`, `subway_station`, `bus_station`, `airport`, `transit_station` |
|
|
95
|
+
| Culture | `museum`, `art_gallery`, `library`, `church`, `temple`, `shrine` |
|
|
96
|
+
| Nature | `park`, `natural_feature`, `campground`, `amusement_park` |
|
|
97
|
+
| Health | `hospital`, `pharmacy`, `doctor`, `dentist` |
|
|
98
|
+
| Shopping | `shopping_mall`, `supermarket`, `convenience_store`, `clothing_store` |
|
|
99
|
+
| Finance | `bank`, `atm` |
|
|
100
|
+
| Education | `school`, `university` |
|
|
101
|
+
| Government | `local_government_office`, `post_office`, `police` |
|
|
102
|
+
|
|
103
|
+
**How to pick the right type for search:**
|
|
104
|
+
- Be specific for precision: `ramen_restaurant` > `restaurant` when you know what you want
|
|
105
|
+
- Use broader types for exploration: `food` catches everything edible
|
|
106
|
+
- Some types are not searchable (too broad) — prefer specific leaf-level types
|
|
107
|
+
- Compound queries work: pass `keyword="ramen"` with `type="restaurant"` for best results
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 5. Routing and Navigation
|
|
112
|
+
|
|
113
|
+
**Travel modes:**
|
|
114
|
+
|
|
115
|
+
| Mode | Parameter | Notes |
|
|
116
|
+
|------|-----------|-------|
|
|
117
|
+
| Driving | `DRIVE` | Default, uses current traffic |
|
|
118
|
+
| Walking | `WALK` | No highways, includes pedestrian paths |
|
|
119
|
+
| Cycling | `BICYCLE` | Not available in all regions |
|
|
120
|
+
| Transit | `TRANSIT` | Requires `departure_time` for accurate results |
|
|
121
|
+
|
|
122
|
+
**Key rule for transit**: Always provide `departure_time` (Unix timestamp). Without it, Google may use default schedules that don't reflect actual service patterns. For planning tools, use a future timestamp on a weekday.
|
|
123
|
+
|
|
124
|
+
**Overview polyline:**
|
|
125
|
+
- A compressed string encoding the entire route path (e.g., `_p~iF~ps|U_ulLnnqC_mqNvxq`@`)
|
|
126
|
+
- Encoded in Google's Polyline Algorithm (each character represents coordinate delta)
|
|
127
|
+
- Use it when you need to pass the route to `maps_static_map` for visualization
|
|
128
|
+
- Do not try to decode it manually — pass it as-is to display tools
|
|
129
|
+
|
|
130
|
+
**Waypoints and stops:**
|
|
131
|
+
- Routes can include intermediate waypoints
|
|
132
|
+
- Optimize waypoint order with `optimize=true` for multi-stop itineraries
|
|
133
|
+
- For Tokyo sightseeing: always think about natural geographic flow (e.g., north→south or circular) to minimize backtracking
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 6. Map Projections
|
|
138
|
+
|
|
139
|
+
**Mercator projection** is what Google Maps (and virtually all web maps) uses.
|
|
140
|
+
|
|
141
|
+
Key distortion property: **area is not preserved, shape is**. The further from the equator, the larger things appear relative to their true size.
|
|
142
|
+
|
|
143
|
+
| Location | Apparent size on map vs reality |
|
|
144
|
+
|----------|--------------------------------|
|
|
145
|
+
| Africa vs Greenland | Africa is 14x larger in reality, but they look similar on Mercator |
|
|
146
|
+
| Japan (~35°N) | Moderate distortion, cities appear roughly accurate |
|
|
147
|
+
| Scandinavia (~60°N) | Significantly overstated on map |
|
|
148
|
+
|
|
149
|
+
**Why this matters for `maps_static_map`:**
|
|
150
|
+
- At the same zoom level, a 600x400 tile covers more actual ground at higher latitudes
|
|
151
|
+
- Zoom 12 in Tokyo covers ~10 km across; zoom 12 in Tokyo's northern suburbs covers slightly more
|
|
152
|
+
- Zoom guidelines for `maps_static_map`:
|
|
153
|
+
|
|
154
|
+
| Zoom | Coverage |
|
|
155
|
+
|------|---------|
|
|
156
|
+
| 1 | World |
|
|
157
|
+
| 5 | Continent/large country |
|
|
158
|
+
| 10 | City |
|
|
159
|
+
| 12 | District |
|
|
160
|
+
| 14 | Neighborhood |
|
|
161
|
+
| 16 | Streets |
|
|
162
|
+
| 18 | Building-level |
|
|
163
|
+
| 20 | Room-level (where available) |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 7. Spatial Search Types
|
|
168
|
+
|
|
169
|
+
**Circular search (radius-based):**
|
|
170
|
+
- Definition: find places within N meters of a center point
|
|
171
|
+
- API: `maps_search_nearby` with `radius` parameter
|
|
172
|
+
- Best for: "coffee shops near me," "ATM within 500m"
|
|
173
|
+
- Limitation: covers uniform area regardless of walkability — a 500m radius includes both sides of a river
|
|
174
|
+
|
|
175
|
+
**Search along route:**
|
|
176
|
+
- Definition: find places near any point on a route path
|
|
177
|
+
- Requires: route polyline from `maps_directions`
|
|
178
|
+
- Best for: "rest stops between Tokyo and Osaka," "gas stations on the way"
|
|
179
|
+
- Implementation: buffer the polyline, search at interval waypoints
|
|
180
|
+
|
|
181
|
+
**Bounding box search:**
|
|
182
|
+
- Definition: find everything within a lat/lng rectangle
|
|
183
|
+
- Best for: "all museums in Kyoto" (known geographic area)
|
|
184
|
+
- Less precise than radius — corners are farther from center than edge midpoints
|
|
185
|
+
|
|
186
|
+
**Choosing the right approach:**
|
|
187
|
+
|
|
188
|
+
| Scenario | Use |
|
|
189
|
+
|----------|-----|
|
|
190
|
+
| Near a specific point | Circular (radius) |
|
|
191
|
+
| Along a travel path | Route-based |
|
|
192
|
+
| Within a city/district | Bounding box or large radius from city center |
|
|
193
|
+
| "Best of" in an area | Keyword search + type filter |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 8. Travel Planning Domain Knowledge
|
|
198
|
+
|
|
199
|
+
**Time-of-day sensitivity:**
|
|
200
|
+
- Early morning (6–9 AM): temples/shrines are quieter, markets open
|
|
201
|
+
- Midday (11 AM–2 PM): lunch crowds at restaurants; museums uncrowded
|
|
202
|
+
- Late afternoon (3–5 PM): good for viewpoints and gardens (golden hour light)
|
|
203
|
+
- Evening (5–8 PM): best for dining, night markets, illuminated landmarks
|
|
204
|
+
- Night (8 PM+): limited temple access, izakayas peak, Tokyo Skytree views
|
|
205
|
+
|
|
206
|
+
**Arc routing principle:**
|
|
207
|
+
- Design routes that move in one general direction, then return — avoid ping-pong backtracking
|
|
208
|
+
- Example: Asakusa → Ueno → Akihabara (east→center→east) is efficient; Shinjuku → Asakusa → Shinjuku is not
|
|
209
|
+
|
|
210
|
+
**Energy curve (fatigue model):**
|
|
211
|
+
- Morning: high energy → schedule physically demanding activities (hills, many stairs)
|
|
212
|
+
- Midday: lunch + rest → schedule museum interiors, air-conditioned venues
|
|
213
|
+
- Afternoon: moderate energy → walking tours, shopping
|
|
214
|
+
- Evening: low energy → seated dining, short walks only
|
|
215
|
+
|
|
216
|
+
**Meal timing:**
|
|
217
|
+
- Breakfast: 7–9 AM
|
|
218
|
+
- Lunch: 11:30 AM–1:30 PM (plan to arrive before 12 to avoid queues)
|
|
219
|
+
- Dinner: 6–8 PM (popular spots fill by 6:30)
|
|
220
|
+
- Always build 15–20 min buffer for transit between meals and activities
|
|
221
|
+
|
|
222
|
+
**Group size adjustments:**
|
|
223
|
+
- Solo/couple: access anywhere, no reservation usually needed
|
|
224
|
+
- Group (4–8): book restaurants ahead, check venue capacity
|
|
225
|
+
- Large group (8+): many historic sites have capacity limits (e.g., Fushimi Inari paths are narrow)
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 9. Japan-Specific Knowledge
|
|
230
|
+
|
|
231
|
+
### Kyoto Area Structure
|
|
232
|
+
|
|
233
|
+
Kyoto's main sightseeing zones:
|
|
234
|
+
|
|
235
|
+
| Zone | Key sites | Character |
|
|
236
|
+
|------|----------|-----------|
|
|
237
|
+
| Arashiyama (west) | Bamboo Grove, Tenryu-ji | Nature, bamboo, riverside |
|
|
238
|
+
| Higashiyama (east) | Kiyomizu-dera, Gion, Ninen-zaka | Traditional streets, temples |
|
|
239
|
+
| Fushimi (south) | Fushimi Inari | Torii gates, accessible by JR |
|
|
240
|
+
| Downtown (center) | Nijo Castle, Nishiki Market | Mix of modern and historical |
|
|
241
|
+
| Nishijin (north-center) | Kinkaku-ji, Ryoan-ji | Famous temples, crowded |
|
|
242
|
+
|
|
243
|
+
**Rule**: Arashiyama and Higashiyama are on opposite sides of the city (~1 hr by bus). Do not combine them in a half-day plan.
|
|
244
|
+
|
|
245
|
+
### Train Network
|
|
246
|
+
|
|
247
|
+
| Network | Type | Use case |
|
|
248
|
+
|---------|------|---------|
|
|
249
|
+
| Shinkansen (bullet train) | Inter-city | Tokyo↔Kyoto (2h15m), Tokyo↔Osaka (2h30m) |
|
|
250
|
+
| JR lines | Regional/urban | JR Pass compatible, connects major stations |
|
|
251
|
+
| Hankyu/Keihan/Kintetsu | Private railways | Osaka↔Kyoto alternatives, often cheaper |
|
|
252
|
+
| Tokyo Metro / Toei | Urban subway | Dense Tokyo inner-city coverage |
|
|
253
|
+
| Kyoto Bus | Urban bus | Essential for Kyoto (no subway to Arashiyama/Fushimi) |
|
|
254
|
+
|
|
255
|
+
**Practical notes:**
|
|
256
|
+
- IC cards (Suica, Pasmo, ICOCA) work on almost all trains and buses in Japan — recommend for all visitors
|
|
257
|
+
- JR Pass only covers JR-operated lines, not private railways or subways
|
|
258
|
+
- In Kyoto, bus day pass (¥700) is cost-effective for 3+ bus rides
|
|
259
|
+
|
|
260
|
+
### Temple and Shrine Access Times
|
|
261
|
+
|
|
262
|
+
| Category | Typical hours | Notes |
|
|
263
|
+
|----------|--------------|-------|
|
|
264
|
+
| Major temples (paid) | 8:30 AM – 5:00 PM | Last entry 30 min before close |
|
|
265
|
+
| Fushimi Inari | 24 hours | Lower section always open |
|
|
266
|
+
| Buddhist temple grounds | 6:00 AM – dusk | Gates/halls may have separate hours |
|
|
267
|
+
| Shrine precincts | Usually always open | Inner buildings have set hours |
|
|
268
|
+
|
|
269
|
+
**Key rule**: Always plan temple visits for morning to avoid afternoon crowds and ensure all halls are open. Schedule Fushimi Inari early morning (6–8 AM) to avoid tour groups.
|
|
270
|
+
|
|
271
|
+
### Japanese Address System
|
|
272
|
+
|
|
273
|
+
Addresses in Japan follow a reverse order from Western convention:
|
|
274
|
+
- Prefecture → City → Ward → Chome (district) → Block → Building
|
|
275
|
+
- Example: `東京都渋谷区道玄坂1丁目2-3` = Tokyo-to, Shibuya-ku, Dogenzaka 1-chome, block 2, building 3
|
|
276
|
+
- Always geocode Japanese addresses using `maps_geocode` — do not attempt to calculate coordinates from address text.
|
|
277
|
+
|
|
278
|
+
### Useful Distance References (Japan)
|
|
279
|
+
|
|
280
|
+
| Route | Distance | Transport | Time |
|
|
281
|
+
|-------|----------|-----------|------|
|
|
282
|
+
| Tokyo → Kyoto | 450 km | Nozomi Shinkansen | 2h15m |
|
|
283
|
+
| Tokyo → Osaka | 520 km | Nozomi Shinkansen | 2h30m |
|
|
284
|
+
| Kyoto → Nara | 35 km | JR Nara Line | 45m |
|
|
285
|
+
| Kyoto → Osaka | 75 km | JR/Hankyu | 15–28m |
|
|
286
|
+
| Shinjuku → Asakusa (Tokyo) | 10 km | Metro | 30m |
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Google Maps API Guide
|
|
2
|
+
|
|
3
|
+
## APIs in Use
|
|
4
|
+
|
|
5
|
+
| API | Endpoint | Tool(s) | GCP Service to Enable |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| Geocoding API | via `@googlemaps/google-maps-services-js` SDK | `maps_geocode`, `maps_reverse_geocode`, `maps_batch_geocode` | Geocoding API |
|
|
8
|
+
| Directions API | via SDK | `maps_directions`, `maps_plan_route` (legs), `maps_search_along_route` (step 1) | Directions API |
|
|
9
|
+
| Distance Matrix API | via SDK | `maps_distance_matrix`, `maps_plan_route` (optimization step) | Distance Matrix API |
|
|
10
|
+
| Elevation API | via SDK | `maps_elevation` | Elevation API |
|
|
11
|
+
| Time Zone API | via SDK | `maps_timezone` | Time Zone API |
|
|
12
|
+
| Places API (New) — Nearby Search | `https://places.googleapis.com/v1/places:searchNearby` (gRPC via `@googlemaps/places`) | `maps_search_nearby` | Places API (New) |
|
|
13
|
+
| Places API (New) — Text Search | `https://places.googleapis.com/v1/places:searchText` (gRPC + REST) | `maps_search_places`, `maps_compare_places`, `maps_explore_area`, `maps_search_along_route` (step 2) | Places API (New) |
|
|
14
|
+
| Places API (New) — Place Details | `https://places.googleapis.com/v1/places/{placeId}` (gRPC) | `maps_place_details` | Places API (New) |
|
|
15
|
+
| Weather API | `https://weather.googleapis.com/v1/currentConditions:lookup` | `maps_weather` (type=current) | Google Weather API |
|
|
16
|
+
| Weather API — Daily Forecast | `https://weather.googleapis.com/v1/forecast/days:lookup` | `maps_weather` (type=forecast_daily, max 10 days) | Google Weather API |
|
|
17
|
+
| Weather API — Hourly Forecast | `https://weather.googleapis.com/v1/forecast/hours:lookup` | `maps_weather` (type=forecast_hourly, max 240 hours) | Google Weather API |
|
|
18
|
+
| Air Quality API | `https://airquality.googleapis.com/v1/currentConditions:lookup` | `maps_air_quality` | Air Quality API |
|
|
19
|
+
| Maps Static API | `https://maps.googleapis.com/maps/api/staticmap` | `maps_static_map` | Maps Static API |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## API Coverage Limitations
|
|
24
|
+
|
|
25
|
+
### Weather API
|
|
26
|
+
|
|
27
|
+
- **Not supported**: China, Japan, South Korea, Cuba, Iran, North Korea, Syria
|
|
28
|
+
- Best coverage: North America, Europe, Oceania
|
|
29
|
+
- Error message when unsupported: `"not supported for this location"` — the code catches this and returns a user-readable message including the list of unsupported regions
|
|
30
|
+
- Forecast limits: daily max 10 days, hourly max 240 hours (enforced with `Math.min/Math.max`)
|
|
31
|
+
|
|
32
|
+
### Air Quality API
|
|
33
|
+
|
|
34
|
+
- Global coverage (no known hard exclusions)
|
|
35
|
+
- Returns multiple indexes: universal AQI + local country-specific index where available
|
|
36
|
+
- `extraComputations` options: `HEALTH_RECOMMENDATIONS`, `POLLUTANT_CONCENTRATION`
|
|
37
|
+
|
|
38
|
+
### Places API (New)
|
|
39
|
+
|
|
40
|
+
- `maxResultCount` hard cap: 20 (enforced in `NewPlacesService`)
|
|
41
|
+
- `searchNearby` uses `includedTypes` (type-based), not free-text keyword
|
|
42
|
+
- Photos: returns `photo.name` (resource reference), not a direct image URL
|
|
43
|
+
|
|
44
|
+
### Directions API
|
|
45
|
+
|
|
46
|
+
- `transit` mode requires `departure_time` for accurate transit schedule results (without it Google may return estimated or no results)
|
|
47
|
+
- `plan_route` optimization uses `driving` mode for distance matrix even when the final leg mode is `transit`, to avoid matrix returning null entries
|
|
48
|
+
|
|
49
|
+
### Static Maps API
|
|
50
|
+
|
|
51
|
+
- URL length limit: 16,384 characters (enforced before fetch)
|
|
52
|
+
- Returns binary PNG, converted to base64 by the server for MCP response
|
|
53
|
+
- `maptype` options: `roadmap`, `satellite`, `terrain`, `hybrid`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Common Pitfalls
|
|
58
|
+
|
|
59
|
+
| Scenario | Problem | Fix |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `transit` directions with no `departure_time` | Google may return `ZERO_RESULTS` or incorrect duration | Always pass `departure_time` (Unix timestamp or `"now"`) when using transit mode |
|
|
62
|
+
| `plan-route` with `formatted_address` from geocode | Directions API may return `ZERO_RESULTS` on complex formatted addresses | `plan-route` passes the original user-provided stop name to Directions, not `formatted_address` from geocode step |
|
|
63
|
+
| `searchNearby` with free-text keyword | `includedTypes` expects a Place type string (e.g. `"restaurant"`), not a general query | Use `search_places` for free-text; use `search_nearby` for type-constrained radius search |
|
|
64
|
+
| Weather for Japan/China | Returns HTTP error with "not supported for this location" | Catch and re-throw with explicit unsupported country list (already implemented) |
|
|
65
|
+
| Air Quality `includePollutants: false` | Default is `true` for `includeHealthRecommendations`, `false` for pollutants | Be explicit — omitting `includePollutants` defaults to `false` |
|
|
66
|
+
| Static map URL over 16,384 chars | Google rejects the request | Reduce number of markers or path waypoints |
|
|
67
|
+
| Place Details with Places API (New) | Resource name format is `places/<id>` not raw `placeId` | `NewPlacesService.getPlaceDetails()` prepends `places/` automatically |
|
|
68
|
+
| Batch geocode concurrency | Default concurrency is 20, max enforced at 50 | Use `--concurrency` flag in CLI batch-geocode command; tool-mode uses `Promise.all` |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Rate Limits and Batch Strategy
|
|
73
|
+
|
|
74
|
+
| API | QPS Limit (default) | Notes |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| Geocoding API | 50 QPS | `maps_batch_geocode` uses `Promise.all` in tool mode; CLI uses semaphore with configurable concurrency (default 20, max 50) |
|
|
77
|
+
| Places API (New) | 100 QPS | Result count capped at 20 per request |
|
|
78
|
+
| Directions API | 50 QPS | `plan_route` makes N-1 serial Directions calls after parallel geocoding |
|
|
79
|
+
| Distance Matrix API | 100 elements/request, 100 QPS | `plan_route` optimization: N×N matrix for ≤ ~10 stops is safe |
|
|
80
|
+
| Weather / Air Quality | Per project quota | No internal retry logic; HTTP 429 surfaces as `"API quota exceeded"` message |
|
|
81
|
+
| Static Maps API | 500 QPS | Single image fetch; no batch |
|
|
82
|
+
|
|
83
|
+
**HTTP 403** → API key invalid or the required GCP API not enabled.
|
|
84
|
+
**HTTP 429 / `OVER_QUERY_LIMIT`** → Quota exceeded; wait and retry or upgrade billing plan.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Search Along Route
|
|
89
|
+
|
|
90
|
+
`maps_search_along_route` is a two-step composite:
|
|
91
|
+
|
|
92
|
+
**Step 1** — Get route polyline via Directions API:
|
|
93
|
+
```
|
|
94
|
+
GoogleMapsTools.getDirections(origin, destination, mode)
|
|
95
|
+
-> routes[0].overview_polyline.points (encoded polyline)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Step 2** — Places Text Search with `searchAlongRouteParameters`:
|
|
99
|
+
```
|
|
100
|
+
POST https://places.googleapis.com/v1/places:searchText
|
|
101
|
+
Headers:
|
|
102
|
+
X-Goog-Api-Key: <key>
|
|
103
|
+
X-Goog-FieldMask: places.displayName,places.id,places.formattedAddress,
|
|
104
|
+
places.location,places.rating,places.userRatingCount,
|
|
105
|
+
places.currentOpeningHours.openNow
|
|
106
|
+
Body:
|
|
107
|
+
{
|
|
108
|
+
"textQuery": "<query>",
|
|
109
|
+
"searchAlongRouteParameters": {
|
|
110
|
+
"polyline": { "encodedPolyline": "<encoded>" }
|
|
111
|
+
},
|
|
112
|
+
"maxResultCount": <1-20>
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Limitations**:
|
|
117
|
+
- Requires Places API (New) to be enabled
|
|
118
|
+
- `maxResults` capped at 20
|
|
119
|
+
- Uses REST fetch (not gRPC SDK) because `searchAlongRouteParameters` is not yet exposed in the Node.js gRPC client
|
|
120
|
+
- Mode defaults to `walking` if not specified
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Places API New vs Legacy
|
|
125
|
+
|
|
126
|
+
| Aspect | Places API (New) | Places API (Legacy) |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| GCP service name | "Places API (New)" | "Places API" |
|
|
129
|
+
| Node.js library | `@googlemaps/places` | `@googlemaps/google-maps-services-js` |
|
|
130
|
+
| Protocol | gRPC (+ REST for searchText) | HTTPS REST |
|
|
131
|
+
| Auth | `apiKey` in constructor / `X-Goog-Api-Key` header | `key` query param |
|
|
132
|
+
| Field selection | `X-Goog-FieldMask` header | `fields` param |
|
|
133
|
+
| Resource naming | `places/<id>` | raw `placeId` string |
|
|
134
|
+
| Tools using it | `maps_search_nearby`, `maps_search_places`, `maps_place_details`, `maps_explore_area`, `maps_compare_places`, `maps_search_along_route` (step 2) | geocode, reverseGeocode, directions, distanceMatrix, elevation, timezone |
|
|
135
|
+
| Max results | 20 per request | varies by endpoint |
|
|
136
|
+
| Photos | Returns `photo.name` resource path | Returns `photo_reference` string |
|
|
137
|
+
| Error codes | gRPC status codes (7=PERMISSION_DENIED, 8=RESOURCE_EXHAUSTED) | HTTP status codes |
|
|
138
|
+
|
|
139
|
+
**Note**: `maps_search_along_route` uses the Legacy Directions API for step 1 (polyline extraction) and Places API (New) REST for step 2 (search). Both APIs must be enabled for this tool to function.
|
package/dist/chunk-TP4VNBCV.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Client as x,Language as A}from"@googlemaps/google-maps-services-js";import R from"dotenv";R.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=A.en;if(this.client=new x({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],a=r.geometry.location;return{lat:a.lat,lng:a.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw h.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}parseCoordinates(e){let t=e.split(",").map(r=>parseFloat(r.trim()));if(t.length!==2||isNaN(t[0])||isNaN(t[1]))throw new Error(`Invalid coordinate format: "${e}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:t[0],lng:t[1]}}async getLocation(e){return e.isCoordinates?this.parseCoordinates(e.value):this.geocodeAddress(e.value)}async geocode(e){try{let t=await this.geocodeAddress(e);return{location:{lat:t.lat,lng:t.lng},formatted_address:t.formatted_address||"",place_id:t.place_id||""}}catch(t){throw h.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let a=r.data.results[0];return{formatted_address:a.formatted_address,place_id:a.place_id,address_components:a.address_components}}catch(r){throw h.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${f(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let n=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(n.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${n.status}`);let s=[],u=[];return n.rows.forEach(l=>{let o=[],c=[];l.elements.forEach(d=>{d.status==="OK"?(o.push({value:d.distance.value,text:d.distance.text}),c.push({value:d.duration.value,text:d.duration.text})):(o.push(null),c.push(null))}),s.push(o),u.push(c)}),{distances:s,durations:u,origin_addresses:n.origin_addresses,destination_addresses:n.destination_addresses}}catch(a){throw h.error("Error in calculateDistanceMatrix:",a),new Error(`Failed to calculate distance matrix: ${f(a)}`)}}async getDirections(e,t,r="driving",a,n){try{let s;n&&(s=Math.floor(n.getTime()/1e3));let u;s||(a instanceof Date?u=Math.floor(a.getTime()/1e3):a?u=a:u="now");let o=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:u}})).data;if(o.status!=="OK")throw new Error(`Failed to get directions with status: ${o.status} (arrival_time: ${s}, departure_time: ${u}`);if(o.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let c=o.routes[0],d=c.legs[0],m=i=>{if(!i||typeof i.value!="number")return"";let g=new Date(i.value*1e3),p={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return i.time_zone&&typeof i.time_zone=="string"&&(p.timeZone=i.time_zone),g.toLocaleString(this.defaultLanguage.toString(),p)};return{routes:o.routes,summary:c.summary,total_distance:{value:d.distance.value,text:d.distance.text},total_duration:{value:d.duration.value,text:d.duration.text},arrival_time:m(d.arrival_time),departure_time:m(d.departure_time)}}catch(s){throw h.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${f(s)}`)}}async getWeather(e,t,r="current",a,n){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,u;switch(r){case"forecast_daily":{let c=Math.min(Math.max(a||5,1),10);u=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${c}`;break}case"forecast_hourly":{let c=Math.min(Math.max(n||24,1),240);u=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${c}`;break}default:u=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let l=await fetch(u);if(!l.ok){let d=(await l.json().catch(()=>({})))?.error?.message||`HTTP ${l.status}`;throw d.includes("not supported for this location")?new Error(`Weather data is not available for this location (${e}, ${t}). The Google Weather API has limited coverage \u2014 China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. Try a location in North America, Europe, or Oceania.`):new Error(d)}let o=await l.json();return r==="current"?{temperature:o.temperature,feelsLike:o.feelsLikeTemperature,humidity:o.relativeHumidity,wind:o.wind,conditions:o.weatherCondition?.description?.text||o.weatherCondition?.type,uvIndex:o.uvIndex,precipitation:o.precipitation,visibility:o.visibility,pressure:o.airPressure,cloudCover:o.cloudCover,isDayTime:o.isDaytime}:o}catch(s){throw h.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,a=!1){try{let n=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),a&&s.push("POLLUTANT_CONCENTRATION");let u={location:{latitude:e,longitude:t}};s.length>0&&(u.extraComputations=s);let l=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)});if(!l.ok){let g=(await l.json().catch(()=>({})))?.error?.message||`HTTP ${l.status}`;throw new Error(g)}let o=await l.json(),c=o.indexes||[],d=c[0],m={dateTime:o.dateTime,regionCode:o.regionCode,aqi:d?.aqi,category:d?.category,dominantPollutant:d?.dominantPollutant,color:d?.color};return c.length>1&&(m.indexes=c.map(i=>({code:i.code,displayName:i.displayName,aqi:i.aqi,category:i.category,dominantPollutant:i.dominantPollutant}))),o.healthRecommendations&&(m.healthRecommendations=o.healthRecommendations),o.pollutants&&(m.pollutants=o.pollutants.map(i=>({code:i.code,displayName:i.displayName,concentration:i.concentration,additionalInfo:i.additionalInfo}))),m}catch(n){throw h.error("Error in getAirQuality:",n),new Error(n.message||`Failed to get air quality for (${e}, ${t})`)}}async getStaticMap(e){try{let t=e.size||"600x400",r=[`key=${this.apiKey}`,`size=${t}`,`maptype=${e.maptype||"roadmap"}`];if(e.center&&r.push(`center=${encodeURIComponent(e.center)}`),e.zoom!==void 0&&r.push(`zoom=${e.zoom}`),e.markers)for(let o of e.markers)r.push(`markers=${encodeURIComponent(o)}`);if(e.path)for(let o of e.path)r.push(`path=${encodeURIComponent(o)}`);let a=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(a.length>16384)throw new Error(`URL exceeds 16,384 character limit (${a.length}). Reduce markers or path points.`);let n=await fetch(a);if(!n.ok){let o=n.headers.get("content-type")||"";if(o.includes("application/json")||o.includes("text/")){let c=await n.text();throw new Error(`Static Maps API error: ${c}`)}throw new Error(`Static Maps API returned HTTP ${n.status}`)}let s=await n.arrayBuffer(),u=Buffer.from(s);return{base64:u.toString("base64"),size:u.length,dimensions:t}}catch(t){throw h.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let a=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:a,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let u=(s.rawOffset+s.dstOffset)*1e3,l=new Date(a*1e3+u).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:l}}catch(a){throw h.error("Error in getTimezone:",a),new Error(`Failed to get timezone for (${e}, ${t}): ${f(a)}`)}}async getElevation(e){try{let t=e.map(n=>({lat:n.latitude,lng:n.longitude})),a=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(a.status!=="OK")throw new Error(`Failed to get elevation data with status: ${a.status}`);return a.results.map((n,s)=>({elevation:n.elevation,location:t[s]}))}catch(t){throw h.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${f(t)}`)}}};import{PlacesClient as $}from"@googlemaps/places";var _=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","photos.heightPx","photos.widthPx","photos.name"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow"].join(",");if(this.client=new $({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(a=>this.transformSearchResult(a))}catch(t){throw h.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(a=>this.transformSearchResult(a))}catch(t){throw h.error("Error in searchText (New API):",t),new Error(`Failed to search places: ${this.extractErrorMessage(t)}`)}}async getPlaceDetails(e){try{let t=`places/${e}`,[r]=await this.client.getPlace({name:t,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(r)}catch(t){throw h.error("Error in getPlaceDetails (New API):",t),new Error(`Failed to get place details for ${e}: ${this.extractErrorMessage(t)}`)}}transformSearchResult(e){return{name:e.displayName?.text||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){return{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:e.regularOpeningHours?{open_now:this.isCurrentlyOpen(e.regularOpeningHours,e.utcOffsetMinutes,e.currentOpeningHours),weekday_text:this.formatOpeningHours(e.regularOpeningHours)}:void 0,formatted_phone_number:e.nationalPhoneNumber||"",website:e.websiteUri||"",price_level:e.priceLevel||0,reviews:e.reviews?.map(t=>({rating:t.rating||0,text:t.text?.text||"",time:t.publishTime?.seconds||0,author_name:t.authorAttribution?.displayName||""}))||[],photos:e.photos?.map(t=>({photo_reference:t.name||"",height:t.heightPx||0,width:t.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let a=e?.periods;if(!Array.isArray(a)||a.length===0)return!1;let n=1440,s=n*7,{day:u,minutes:l}=this.getLocalTimeComponents(t),o=u*n+l,c={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},d=i=>{if(typeof i=="number"&&i>=0&&i<=6)return i;if(typeof i=="string"){let g=i.toUpperCase();if(g in c)return c[g]}},m=i=>{if(!i)return;let g=typeof i.hours=="number"?i.hours:Number(i.hours??NaN),p=typeof i.minutes=="number"?i.minutes:Number(i.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(p)))return g*60+p};for(let i of a){let g=d(i?.openDay),p=d(i?.closeDay??i?.openDay),w=m(i?.openTime),T=m(i?.closeTime);if(g===void 0||w===void 0)continue;let v=g*n+w,b;p===void 0||T===void 0?b=v+n:b=p*n+T,b<=v&&(b+=s);let P=o;for(;P<v;)P+=s;if(P>=v&&P<b)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var N=class{constructor(e){this.mapsTools=new E(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),a=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(a=a.filter(n=>n.opening_hours?.open_now===!0)),e.minRating&&(a=a.filter(n=>(n.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:a.map(n=>({name:n.name,place_id:n.place_id,address:n.formatted_address,location:n.geometry.location,rating:n.rating,total_ratings:n.user_ratings_total,open_now:n.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during search"}}}async searchText(e){try{return{success:!0,data:(await this.newPlacesService.searchText({textQuery:e.query,locationBias:e.locationBias?{lat:e.locationBias.latitude,lng:e.locationBias.longitude,radius:e.locationBias.radius}:void 0,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType})).map(r=>({name:r.name,place_id:r.place_id,address:r.formatted_address,location:r.geometry.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during text search"}}}async getPlaceDetails(e){try{let t=await this.newPlacesService.getPlaceDetails(e);return{success:!0,data:{name:t.name,address:t.formatted_address,location:t.geometry?.location,rating:t.rating,total_ratings:t.user_ratings_total,open_now:t.opening_hours?.open_now,phone:t.formatted_phone_number,website:t.website,price_level:t.price_level,reviews:t.reviews?.map(r=>({rating:r.rating,text:r.text,time:r.time,author_name:r.author_name}))}}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(e,t,r)}}catch(a){return{success:!1,error:a instanceof Error?a.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",a,n){try{let s=a?new Date(a):new Date,u=n?new Date(n):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,u)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(a){return{success:!1,error:a instanceof Error?a.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",a,n){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,a,n)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,a){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,a)}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,a=e.topN||3,n=await this.geocode(e.location);if(!n.success||!n.data)throw new Error(n.error||"Geocode failed");let{lat:s,lng:u}=n.data.location,l=[];for(let o of t){let c=await this.searchNearby({center:{value:`${s},${u}`,isCoordinates:!0},keyword:o,radius:r});if(!c.success||!c.data)continue;let d=c.data.slice(0,a),m=[];for(let i of d){if(!i.place_id)continue;let g=await this.getPlaceDetails(i.place_id);m.push({name:i.name,address:i.address,rating:i.rating,total_ratings:i.total_ratings,open_now:i.open_now,phone:g.data?.phone,website:g.data?.website})}l.push({type:o,count:c.data.length,top:m})}return{success:!0,data:{location:{address:n.data.formatted_address,lat:s,lng:u},radius:r,categories:l}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let a=[];for(let o of r){let c=await this.geocode(o);if(!c.success||!c.data)throw new Error(`Failed to geocode: ${o}`);a.push({originalName:o,address:c.data.formatted_address,lat:c.data.location.lat,lng:c.data.location.lng})}let n=a;if(e.optimize!==!1&&a.length>2){let o=await this.calculateDistanceMatrix(r,r,"driving");if(o.success&&o.data){let c=new Set([0]),d=[0],m=0;for(;c.size<a.length;){let i=-1,g=1/0;for(let p=0;p<a.length;p++){if(c.has(p))continue;let w=o.data.durations[m]?.[p]?.value??1/0;w<g&&(g=w,i=p)}if(i===-1)break;c.add(i),d.push(i),m=i}n=d.map(i=>a[i])}}let s=[],u=0,l=0;for(let o=0;o<n.length-1;o++){let c=await this.getDirections(n[o].originalName,n[o+1].originalName,t);c.success&&c.data?(u+=c.data.total_distance.value,l+=c.data.total_duration.value,s.push({from:n[o].originalName,to:n[o+1].originalName,distance:c.data.total_distance.text,duration:c.data.total_duration.text})):s.push({from:n[o].originalName,to:n[o+1].originalName,distance:"unknown",duration:"unknown",note:c.error||"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:e.optimize!==!1&&a.length>2,stops:n.map(o=>`${o.originalName} (${o.address})`),legs:s,total_distance:`${(u/1e3).toFixed(1)} km`,total_duration:`${Math.round(l/60)} min`}}}async comparePlaces(e){let t=e.limit||5,r=await this.searchText({query:e.query});if(!r.success||!r.data)throw new Error(r.error||"Search failed");let a=r.data.slice(0,t),n=[];for(let s of a){let u=await this.getPlaceDetails(s.place_id);n.push({name:s.name,address:s.address,rating:s.rating,total_ratings:s.total_ratings,open_now:s.open_now,phone:u.data?.phone,website:u.data?.website,price_level:u.data?.price_level})}if(e.userLocation&&n.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,u=a.map(o=>`${o.location.lat},${o.location.lng}`),l=await this.calculateDistanceMatrix([s],u,"driving");if(l.success&&l.data)for(let o=0;o<n.length;o++)n[o].distance=l.data.distances[0]?.[o]?.text,n[o].drive_time=l.data.durations[0]?.[o]?.text}return{success:!0,data:n}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var h={log:(...y)=>{console.error("[INFO]",...y)},error:(...y)=>{console.error("[ERROR]",...y)}};export{_ as a,h as b,N as c};
|