|
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_basic] |
| 7 | +// [START maps_ai_powered_summaries] |
| 8 | +// Define DOM elements. |
8 | 9 | const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; |
9 | | -let innerMap; |
10 | | -let infoWindow; |
11 | | - |
12 | | -async function initMap() { |
13 | | - const { Map, InfoWindow } = (await google.maps.importLibrary( |
14 | | - 'maps' |
15 | | - )) as google.maps.MapsLibrary; |
| 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; |
16 | 21 |
|
17 | | - innerMap = mapElement.innerMap; |
18 | | - infoWindow = new InfoWindow(); |
19 | | - getPlaceDetails(); |
20 | | -} |
| 22 | +let innerMap; |
| 23 | +let marker: google.maps.marker.AdvancedMarkerElement; |
21 | 24 |
|
22 | | -async function getPlaceDetails() { |
| 25 | +async function initMap(): Promise<void> { |
23 | 26 | // Request needed libraries. |
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>, |
| 27 | + const [] = await Promise.all([ |
| 28 | + google.maps.importLibrary('marker'), |
| 29 | + google.maps.importLibrary('places'), |
27 | 30 | ]); |
28 | 31 |
|
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 |
| 32 | + innerMap = mapElement.innerMap; |
| 33 | + innerMap.setOptions({ |
| 34 | + mapTypeControl: false, |
| 35 | + streetViewControl: false, |
| 36 | + fullscreenControl: false, |
33 | 37 | }); |
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 | | - ], |
| 38 | + |
| 39 | + // Bind autocomplete bounds to map bounds. |
| 40 | + google.maps.event.addListener(innerMap, 'bounds_changed', async () => { |
| 41 | + placeAutocomplete.locationRestriction = innerMap.getBounds(); |
45 | 42 | }); |
46 | | - // [END maps_ai_powered_summaries_basic_fetchfields] |
47 | 43 |
|
48 | | - // Add an Advanced Marker |
49 | | - const marker = new AdvancedMarkerElement({ |
| 44 | + // Create the marker. |
| 45 | + marker = new google.maps.marker.AdvancedMarkerElement({ |
50 | 46 | map: innerMap, |
51 | | - position: place.location, |
52 | | - title: place.displayName, |
53 | 47 | }); |
54 | 48 |
|
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'); |
| 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 | + } |
61 | 114 |
|
62 | | - // Retrieve the summary text and disclosure text. |
| 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) --- |
63 | 141 | //@ts-ignore |
64 | | - let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; |
| 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 --- |
65 | 153 | //@ts-ignore |
66 | | - let disclosureText = place.generativeSummary.disclosureText ?? ''; |
| 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 | + } |
67 | 163 |
|
68 | | - address.textContent = place.formattedAddress ?? ''; |
69 | | - summary.textContent = `${overviewText} [${disclosureText}]`; |
70 | | - content.append(address, lineBreak, summary);; |
| 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 | + } |
71 | 175 |
|
72 | | - innerMap.setCenter(place.location); |
| 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(); |
73 | 182 |
|
74 | | - // Display an info window. |
75 | | - infoWindow.setHeaderContent(place.displayName); |
76 | | - infoWindow.setContent(content); |
77 | | - infoWindow.open({ |
78 | | - anchor: marker, |
79 | | - }); |
| 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 | + } |
80 | 239 | } |
81 | 240 |
|
82 | 241 | initMap(); |
83 | | -// [END maps_ai_powered_summaries_basic] |
| 242 | +// [END maps_ai_powered_summaries] |
0 commit comments