|
1 | | -/* |
| 1 | +/** |
2 | 2 | * @license |
3 | 3 | * Copyright 2025 Google LLC. All Rights Reserved. |
4 | 4 | * SPDX-License-Identifier: Apache-2.0 |
5 | 5 | */ |
6 | 6 |
|
7 | | -// [START maps_ai_powered_summaries] |
8 | | -// Define DOM elements. |
| 7 | +// [START maps_ai_powered_summaries_basic] |
9 | 8 | const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; |
10 | | -const placeAutocomplete = document.querySelector( |
11 | | - 'gmp-place-autocomplete' |
12 | | -) as google.maps.places.PlaceAutocompleteElement; |
13 | | -const summaryPanel = document.getElementById('summary-panel') as HTMLDivElement; |
14 | | -const placeName = document.getElementById('place-name') as HTMLElement; |
15 | | -const placeAddress = document.getElementById('place-address') as HTMLElement; |
16 | | -const tabContainer = document.getElementById('tab-container') as HTMLDivElement; |
17 | | -const summaryContent = document.getElementById( |
18 | | - 'summary-content' |
19 | | -) as HTMLDivElement; |
20 | | -const aiDisclosure = document.getElementById('ai-disclosure') as HTMLDivElement; |
21 | | - |
22 | 9 | let innerMap; |
23 | | -let marker: google.maps.marker.AdvancedMarkerElement; |
| 10 | +let infoWindow; |
| 11 | + |
| 12 | +async function initMap() { |
| 13 | + const { Map, InfoWindow } = (await google.maps.importLibrary( |
| 14 | + 'maps' |
| 15 | + )) as google.maps.MapsLibrary; |
| 16 | + |
| 17 | + innerMap = mapElement.innerMap; |
| 18 | + infoWindow = new InfoWindow(); |
| 19 | + getPlaceDetails(); |
| 20 | +} |
24 | 21 |
|
25 | | -async function initMap(): Promise<void> { |
| 22 | +async function getPlaceDetails() { |
26 | 23 | // Request needed libraries. |
27 | | - const [] = await Promise.all([ |
28 | | - google.maps.importLibrary('marker'), |
29 | | - google.maps.importLibrary('places'), |
| 24 | + const [ {AdvancedMarkerElement}, { Place } ] = await Promise.all([ |
| 25 | + google.maps.importLibrary('marker') as Promise<google.maps.MarkerLibrary>, |
| 26 | + google.maps.importLibrary('places') as Promise<google.maps.PlacesLibrary>, |
30 | 27 | ]); |
31 | 28 |
|
32 | | - innerMap = mapElement.innerMap; |
33 | | - innerMap.setOptions({ |
34 | | - mapTypeControl: false, |
35 | | - streetViewControl: false, |
36 | | - fullscreenControl: false, |
| 29 | + // [START maps_ai_powered_summaries_basic_placeid] |
| 30 | + // Use place ID to create a new Place instance. |
| 31 | + const place = new Place({ |
| 32 | + id: 'ChIJzzc-aWUM3IARPOQr9sA6vfY', // San Diego Botanic Garden |
37 | 33 | }); |
38 | | - |
39 | | - // Bind autocomplete bounds to map bounds. |
40 | | - google.maps.event.addListener(innerMap, 'bounds_changed', async () => { |
41 | | - placeAutocomplete.locationRestriction = innerMap.getBounds(); |
| 34 | + // [END maps_ai_powered_summaries_basic_placeid] |
| 35 | + |
| 36 | + // Call fetchFields, passing the needed data fields. |
| 37 | + // [START maps_ai_powered_summaries_basic_fetchfields] |
| 38 | + await place.fetchFields({ |
| 39 | + fields: [ |
| 40 | + 'displayName', |
| 41 | + 'formattedAddress', |
| 42 | + 'location', |
| 43 | + 'generativeSummary', |
| 44 | + ], |
42 | 45 | }); |
| 46 | + // [END maps_ai_powered_summaries_basic_fetchfields] |
43 | 47 |
|
44 | | - // Create the marker. |
45 | | - marker = new google.maps.marker.AdvancedMarkerElement({ |
| 48 | + // Add an Advanced Marker |
| 49 | + const marker = new AdvancedMarkerElement({ |
46 | 50 | map: innerMap, |
| 51 | + position: place.location, |
| 52 | + title: place.displayName, |
47 | 53 | }); |
48 | 54 |
|
49 | | - // Handle selection of an autocomplete result. |
50 | | - // prettier-ignore |
51 | | - // @ts-ignore |
52 | | - placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { |
53 | | - const place = placePrediction.toPlace(); |
54 | | - |
55 | | - // Fetch all summary fields. |
56 | | - // [START maps_ai_powered_summaries_fetchfields] |
57 | | - await place.fetchFields({ |
58 | | - fields: [ |
59 | | - 'displayName', |
60 | | - 'formattedAddress', |
61 | | - 'location', |
62 | | - 'generativeSummary', |
63 | | - 'neighborhoodSummary', |
64 | | - 'reviewSummary', |
65 | | - 'evChargeAmenitySummary', |
66 | | - ], |
67 | | - }); |
68 | | - // [END maps_ai_powered_summaries_fetchfields] |
69 | | - |
70 | | - // Update the map viewport and position the marker. |
71 | | - if (place.viewport) { |
72 | | - innerMap.fitBounds(place.viewport); |
73 | | - } else { |
74 | | - innerMap.setCenter(place.location); |
75 | | - innerMap.setZoom(17); |
76 | | - } |
77 | | - marker.position = place.location; |
78 | | - |
79 | | - // Update the panel UI. |
80 | | - updateSummaryPanel(place); |
81 | | - } |
82 | | - ); |
83 | | -} |
84 | | - |
85 | | -function updateSummaryPanel(place: google.maps.places.Place) { |
86 | | - // Reset UI |
87 | | - summaryPanel.classList.remove('hidden'); |
88 | | - tabContainer.innerHTML = ''; // innerHTML is OK here since we're clearing known child elements. |
89 | | - summaryContent.textContent = ''; |
90 | | - aiDisclosure.textContent = ''; |
91 | | - |
92 | | - placeName.textContent = place.displayName || ''; |
93 | | - placeAddress.textContent = place.formattedAddress || ''; |
94 | | - |
95 | | - let firstTabActivated = false; |
96 | | - |
97 | | - /** |
98 | | - * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). |
99 | | - */ |
100 | | - const createTab = ( |
101 | | - label: string, |
102 | | - content: string | Node, |
103 | | - disclosure: string |
104 | | - ) => { |
105 | | - const btn = document.createElement('button'); |
106 | | - btn.className = 'tab-button'; |
107 | | - btn.textContent = label; |
108 | | - |
109 | | - btn.onclick = () => { |
110 | | - // Do nothing if the tab is already active. |
111 | | - if (btn.classList.contains('active')) { |
112 | | - return; |
113 | | - } |
| 55 | + // Create a content container. |
| 56 | + const content = document.createElement('div'); |
| 57 | + // Populate the container with data. |
| 58 | + const address = document.createElement('div'); |
| 59 | + const summary = document.createElement('div'); |
| 60 | + const lineBreak = document.createElement('br'); |
114 | 61 |
|
115 | | - // Manage the active class state. |
116 | | - document |
117 | | - .querySelectorAll('.tab-button') |
118 | | - .forEach((b) => b.classList.remove('active')); |
119 | | - btn.classList.add('active'); |
120 | | - |
121 | | - if (typeof content === 'string') { |
122 | | - summaryContent.textContent = content; |
123 | | - } else { |
124 | | - summaryContent.replaceChildren(content.cloneNode(true)); |
125 | | - } |
126 | | - |
127 | | - // Set the disclosure text. |
128 | | - aiDisclosure.textContent = disclosure || 'AI-generated content.'; |
129 | | - }; |
130 | | - |
131 | | - tabContainer.appendChild(btn); |
132 | | - |
133 | | - // Auto-select the first available summary. |
134 | | - if (!firstTabActivated) { |
135 | | - btn.click(); |
136 | | - firstTabActivated = true; |
137 | | - } |
138 | | - }; |
139 | | - |
140 | | - // --- 1. Generative Summary (Place) --- |
| 62 | + // Retrieve the summary text and disclosure text. |
141 | 63 | //@ts-ignore |
142 | | - if (place.generativeSummary?.overview) { |
143 | | - createTab( |
144 | | - 'Overview', |
145 | | - //@ts-ignore |
146 | | - place.generativeSummary.overview, |
147 | | - //@ts-ignore |
148 | | - place.generativeSummary.disclosureText |
149 | | - ); |
150 | | - } |
151 | | - |
152 | | - // --- 2. Review Summary --- |
| 64 | + let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; |
153 | 65 | //@ts-ignore |
154 | | - if (place.reviewSummary?.text) { |
155 | | - createTab( |
156 | | - 'Reviews', |
157 | | - //@ts-ignore |
158 | | - place.reviewSummary.text, |
159 | | - //@ts-ignore |
160 | | - place.reviewSummary.disclosureText |
161 | | - ); |
162 | | - } |
| 66 | + let disclosureText = place.generativeSummary.disclosureText ?? ''; |
163 | 67 |
|
164 | | - // --- 3. Neighborhood Summary --- |
165 | | - //@ts-ignore |
166 | | - if (place.neighborhoodSummary?.overview?.content) { |
167 | | - createTab( |
168 | | - 'Neighborhood', |
169 | | - //@ts-ignore |
170 | | - place.neighborhoodSummary.overview.content, |
171 | | - //@ts-ignore |
172 | | - place.neighborhoodSummary.disclosureText |
173 | | - ); |
174 | | - } |
| 68 | + address.textContent = place.formattedAddress ?? ''; |
| 69 | + summary.textContent = `${overviewText} [${disclosureText}]`; |
| 70 | + content.append(address, lineBreak, summary);; |
175 | 71 |
|
176 | | - // --- 4. EV Amenity Summary (uses content blocks)) --- |
177 | | - //@ts-ignore |
178 | | - if (place.evChargeAmenitySummary) { |
179 | | - //@ts-ignore |
180 | | - const evSummary = place.evChargeAmenitySummary; |
181 | | - const evContainer = document.createDocumentFragment(); |
| 72 | + innerMap.setCenter(place.location); |
182 | 73 |
|
183 | | - // Helper to build a safe DOM section for EV categories. |
184 | | - const createSection = (title: string, text: string) => { |
185 | | - const wrapper = document.createElement('div'); |
186 | | - wrapper.style.marginBottom = '15px'; // Or use a CSS class |
187 | | - |
188 | | - const titleEl = document.createElement('strong'); |
189 | | - titleEl.textContent = title; |
190 | | - |
191 | | - const textEl = document.createElement('div'); |
192 | | - textEl.textContent = text; |
193 | | - |
194 | | - wrapper.appendChild(titleEl); |
195 | | - wrapper.appendChild(textEl); |
196 | | - return wrapper; |
197 | | - }; |
198 | | - |
199 | | - // Check and append each potential section |
200 | | - if (evSummary.overview?.content) { |
201 | | - evContainer.appendChild( |
202 | | - createSection('Overview', evSummary.overview.content) |
203 | | - ); |
204 | | - } |
205 | | - if (evSummary.coffee?.content) { |
206 | | - evContainer.appendChild( |
207 | | - createSection('Coffee', evSummary.coffee.content) |
208 | | - ); |
209 | | - } |
210 | | - if (evSummary.restaurant?.content) { |
211 | | - evContainer.appendChild( |
212 | | - createSection('Food', evSummary.restaurant.content) |
213 | | - ); |
214 | | - } |
215 | | - if (evSummary.store?.content) { |
216 | | - evContainer.appendChild( |
217 | | - createSection('Shopping', evSummary.store.content) |
218 | | - ); |
219 | | - } |
220 | | - |
221 | | - // Only add the tab if the container has children |
222 | | - if (evContainer.hasChildNodes()) { |
223 | | - createTab( |
224 | | - 'EV Amenities', |
225 | | - evContainer, // Passing a Node instead of string |
226 | | - evSummary.disclosureText |
227 | | - ); |
228 | | - } |
229 | | - } |
230 | | - |
231 | | - // Safely handle the empty state. |
232 | | - if (!firstTabActivated) { |
233 | | - const msg = document.createElement('em'); |
234 | | - msg.textContent = |
235 | | - 'No AI summaries are available for this specific location.'; |
236 | | - summaryContent.replaceChildren(msg); |
237 | | - aiDisclosure.textContent = ''; |
238 | | - } |
| 74 | + // Display an info window. |
| 75 | + infoWindow.setHeaderContent(place.displayName); |
| 76 | + infoWindow.setContent(content); |
| 77 | + infoWindow.open({ |
| 78 | + anchor: marker, |
| 79 | + }); |
239 | 80 | } |
240 | 81 |
|
241 | 82 | initMap(); |
242 | | -// [END maps_ai_powered_summaries] |
| 83 | +// [END maps_ai_powered_summaries_basic] |
0 commit comments