diff --git a/dist/index.html b/dist/index.html index 02f85042..820cd730 100644 --- a/dist/index.html +++ b/dist/index.html @@ -48,6 +48,8 @@

Maps JSAPI Samples

  • advanced-markers-html-simple
  • advanced-markers-simple
  • advanced-markers-zoom
  • +
  • ai-powered-summaries
  • +
  • ai-powered-summaries-basic
  • boundaries-choropleth
  • boundaries-click
  • boundaries-simple
  • diff --git a/dist/samples/ai-powered-summaries-basic/app/.eslintsrc.json b/dist/samples/ai-powered-summaries-basic/app/.eslintsrc.json new file mode 100644 index 00000000..4c44dab0 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/.eslintsrc.json @@ -0,0 +1,13 @@ +{ + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "rules": { + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-this-alias": 1, + "@typescript-eslint/no-empty-function": 1, + "@typescript-eslint/explicit-module-boundary-types": 1, + "@typescript-eslint/no-unused-vars": 1 + } +} diff --git a/dist/samples/ai-powered-summaries-basic/app/README.md b/dist/samples/ai-powered-summaries-basic/app/README.md new file mode 100644 index 00000000..1a4b6847 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/README.md @@ -0,0 +1,36 @@ +# Google Maps JavaScript Sample + +## ai-powered-summaries-basic + +The ai-powered-summaries-basic sample demonstrates how to retrieve AI-powered summaries. + +Follow these instructions to set up and run ai-powered-summaries-basic sample on your local computer. + +## Setup + +### Before starting run: + +`npm i` + +### Run an example on a local web server + +First `cd` to the folder for the sample to run, then: + +`npm start` + +### Build an individual example + +From `samples/`: + +`npm run build --workspace=ai-powered-summaries-basic/` + +### Build all of the examples. + +From `samples/`: + +`npm run build-all` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). diff --git a/dist/samples/ai-powered-summaries-basic/app/index.html b/dist/samples/ai-powered-summaries-basic/app/index.html new file mode 100644 index 00000000..6def208f --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/index.html @@ -0,0 +1,26 @@ + + + + + + AI-powered Summaries Basic Sample + + + + + + + + + + + + diff --git a/dist/samples/ai-powered-summaries-basic/app/index.ts b/dist/samples/ai-powered-summaries-basic/app/index.ts new file mode 100644 index 00000000..58ab067d --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/index.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_ai_powered_summaries_basic] +const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; +let innerMap; +let infoWindow; + +async function initMap() { + const { Map, InfoWindow } = (await google.maps.importLibrary( + 'maps' + )) as google.maps.MapsLibrary; + + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false + }); + + infoWindow = new InfoWindow(); + getPlaceDetails(); +} + +async function getPlaceDetails() { + // Request needed libraries. + const [ {AdvancedMarkerElement}, { Place } ] = await Promise.all([ + google.maps.importLibrary('marker') as Promise, + google.maps.importLibrary('places') as Promise, + ]); + + // [START maps_ai_powered_summaries_basic_placeid] + // Use place ID to create a new Place instance. + const place = new Place({ + id: 'ChIJzzc-aWUM3IARPOQr9sA6vfY', // San Diego Botanic Garden + }); + // [END maps_ai_powered_summaries_basic_placeid] + + // Call fetchFields, passing the needed data fields. + // [START maps_ai_powered_summaries_basic_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + ], + }); + // [END maps_ai_powered_summaries_basic_fetchfields] + + // Add an Advanced Marker + const marker = new AdvancedMarkerElement({ + map: innerMap, + position: place.location, + title: place.displayName, + }); + + // Create a content container. + const content = document.createElement('div'); + // Populate the container with data. + const address = document.createElement('div'); + const summary = document.createElement('div'); + const lineBreak = document.createElement('br'); + const attribution = document.createElement('div'); + + // Retrieve the textual data (summary, disclosure, flag URI). + //@ts-ignore + let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; + //@ts-ignore + let disclosureText = place.generativeSummary.disclosureText; + //@ts-ignore + let reportingUri = place.generativeSummary.flagContentURI; + + // Create HTML for reporting link. + const reportingLink = document.createElement('a'); + reportingLink.href = reportingUri; + reportingLink.target = '_blank'; + reportingLink.textContent = "Report a problem." + + // Add text to layout. + address.textContent = place.formattedAddress ?? ''; + summary.textContent = overviewText; + attribution.textContent = `${disclosureText} `; + attribution.appendChild(reportingLink); + + content.append(address, lineBreak, summary, lineBreak, attribution); + + innerMap.setCenter(place.location); + + // Handle marker click. + marker.addListener('gmp-click', () => { + showInfoWindow(marker, place, content); + }); + + // Display the info window at load time. + showInfoWindow(marker, place, content); +} + +function showInfoWindow(marker, place, content) { + // Display an info window. + infoWindow.setHeaderContent(place.displayName); + infoWindow.setContent(content); + infoWindow.open({ + anchor: marker, + }); +} + +initMap(); +// [END maps_ai_powered_summaries_basic] diff --git a/dist/samples/ai-powered-summaries-basic/app/package.json b/dist/samples/ai-powered-summaries-basic/app/package.json new file mode 100644 index 00000000..06aa6790 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/package.json @@ -0,0 +1,14 @@ +{ + "name": "@js-api-samples/ai-powered-summaries-basic", + "version": "1.0.0", + "scripts": { + "build": "tsc && bash ../jsfiddle.sh ai-powered-summaries-basic && bash ../app.sh ai-powered-summaries-basic && bash ../docs.sh ai-powered-summaries-basic && npm run build:vite --workspace=. && bash ../dist.sh ai-powered-summaries-basic", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --base './' && vite", + "build:vite": "vite build --base './'", + "preview": "vite preview" + }, + "dependencies": { + + } +} diff --git a/dist/samples/ai-powered-summaries-basic/app/style.css b/dist/samples/ai-powered-summaries-basic/app/style.css new file mode 100644 index 00000000..93167d23 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/style.css @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries_basic] */ +/* + * Always set the map height explicitly to define the size of the div element + * that contains the map. + */ +#map { + height: 100%; +} + +/* + * Optional: Makes the sample page fill the window. + */ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* [END maps_ai_powered_summaries_basic] */ diff --git a/dist/samples/ai-powered-summaries-basic/app/tsconfig.json b/dist/samples/ai-powered-summaries-basic/app/tsconfig.json new file mode 100644 index 00000000..366aabb0 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true, + "noImplicitAny": false, + "lib": [ + "es2015", + "esnext", + "es6", + "dom", + "dom.iterable" + ], + "moduleResolution": "Node", + "jsx": "preserve" + } +} diff --git a/dist/samples/ai-powered-summaries-basic/dist/assets/index-CFZ6i2WL.js b/dist/samples/ai-powered-summaries-basic/dist/assets/index-CFZ6i2WL.js new file mode 100644 index 00000000..f2ef289f --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/dist/assets/index-CFZ6i2WL.js @@ -0,0 +1 @@ +(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))o(e);new MutationObserver(e=>{for(const n of e)if(n.type==="childList")for(const i of n.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&o(i)}).observe(document,{childList:!0,subtree:!0});function t(e){const n={};return e.integrity&&(n.integrity=e.integrity),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?n.credentials="include":e.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function o(e){if(e.ep)return;e.ep=!0;const n=t(e);fetch(e.href,n)}})();const g=document.querySelector("gmp-map");let l,c;async function v(){const{Map:a,InfoWindow:r}=await google.maps.importLibrary("maps");l=g.innerMap,l.setOptions({mapTypeControl:!1}),c=new r,h()}async function h(){const[{AdvancedMarkerElement:a},{Place:r}]=await Promise.all([google.maps.importLibrary("marker"),google.maps.importLibrary("places")]),t=new r({id:"ChIJzzc-aWUM3IARPOQr9sA6vfY"});await t.fetchFields({fields:["displayName","formattedAddress","location","generativeSummary"]});const o=new a({map:l,position:t.location,title:t.displayName}),e=document.createElement("div"),n=document.createElement("div"),i=document.createElement("div"),d=document.createElement("br"),m=document.createElement("div");let u=t.generativeSummary.overview??"No summary is available.",f=t.generativeSummary.disclosureText,y=t.generativeSummary.flagContentURI;const s=document.createElement("a");s.href=y,s.target="_blank",s.textContent="Report a problem.",n.textContent=t.formattedAddress??"",i.textContent=u,m.textContent=`${f} `,m.appendChild(s),e.append(n,d,i,d,m),l.setCenter(t.location),o.addListener("gmp-click",()=>{p(o,t,e)}),p(o,t,e)}function p(a,r,t){c.setHeaderContent(r.displayName),c.setContent(t),c.open({anchor:a})}v(); diff --git a/dist/samples/ai-powered-summaries-basic/dist/assets/index-DWepjxzn.css b/dist/samples/ai-powered-summaries-basic/dist/assets/index-DWepjxzn.css new file mode 100644 index 00000000..c49b8a3f --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/dist/assets/index-DWepjxzn.css @@ -0,0 +1 @@ +#map{height:100%}html,body{height:100%;margin:0;padding:0} diff --git a/dist/samples/ai-powered-summaries-basic/dist/index.html b/dist/samples/ai-powered-summaries-basic/dist/index.html new file mode 100644 index 00000000..3dbcdc6c --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/dist/index.html @@ -0,0 +1,26 @@ + + + + + + AI-powered Summaries Basic Sample + + + + + + + + + + + + diff --git a/dist/samples/ai-powered-summaries-basic/docs/index.html b/dist/samples/ai-powered-summaries-basic/docs/index.html new file mode 100644 index 00000000..6def208f --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/docs/index.html @@ -0,0 +1,26 @@ + + + + + + AI-powered Summaries Basic Sample + + + + + + + + + + + + diff --git a/dist/samples/ai-powered-summaries-basic/docs/index.js b/dist/samples/ai-powered-summaries-basic/docs/index.js new file mode 100644 index 00000000..ac1c6e35 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/docs/index.js @@ -0,0 +1,91 @@ +"use strict"; +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// [START maps_ai_powered_summaries_basic] +const mapElement = document.querySelector('gmp-map'); +let innerMap; +let infoWindow; +async function initMap() { + const { Map, InfoWindow } = (await google.maps.importLibrary('maps')); + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false + }); + infoWindow = new InfoWindow(); + getPlaceDetails(); +} +async function getPlaceDetails() { + // Request needed libraries. + const [{ AdvancedMarkerElement }, { Place }] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + // [START maps_ai_powered_summaries_basic_placeid] + // Use place ID to create a new Place instance. + const place = new Place({ + id: 'ChIJzzc-aWUM3IARPOQr9sA6vfY', // San Diego Botanic Garden + }); + // [END maps_ai_powered_summaries_basic_placeid] + // Call fetchFields, passing the needed data fields. + // [START maps_ai_powered_summaries_basic_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + ], + }); + // [END maps_ai_powered_summaries_basic_fetchfields] + // Add an Advanced Marker + const marker = new AdvancedMarkerElement({ + map: innerMap, + position: place.location, + title: place.displayName, + }); + // Create a content container. + const content = document.createElement('div'); + // Populate the container with data. + const address = document.createElement('div'); + const summary = document.createElement('div'); + const lineBreak = document.createElement('br'); + const attribution = document.createElement('div'); + // Retrieve the textual data (summary, disclosure, flag URI). + //@ts-ignore + let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; + //@ts-ignore + let disclosureText = place.generativeSummary.disclosureText; + //@ts-ignore + let reportingUri = place.generativeSummary.flagContentURI; + // Create HTML for reporting link. + const reportingLink = document.createElement('a'); + reportingLink.href = reportingUri; + reportingLink.target = '_blank'; + reportingLink.textContent = "Report a problem."; + // Add text to layout. + address.textContent = place.formattedAddress ?? ''; + summary.textContent = overviewText; + attribution.textContent = `${disclosureText} `; + attribution.appendChild(reportingLink); + content.append(address, lineBreak, summary, lineBreak, attribution); + innerMap.setCenter(place.location); + // Handle marker click. + marker.addListener('gmp-click', () => { + showInfoWindow(marker, place, content); + }); + // Display the info window at load time. + showInfoWindow(marker, place, content); +} +function showInfoWindow(marker, place, content) { + // Display an info window. + infoWindow.setHeaderContent(place.displayName); + infoWindow.setContent(content); + infoWindow.open({ + anchor: marker, + }); +} +initMap(); +// [END maps_ai_powered_summaries_basic] diff --git a/dist/samples/ai-powered-summaries-basic/docs/index.ts b/dist/samples/ai-powered-summaries-basic/docs/index.ts new file mode 100644 index 00000000..58ab067d --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/docs/index.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_ai_powered_summaries_basic] +const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; +let innerMap; +let infoWindow; + +async function initMap() { + const { Map, InfoWindow } = (await google.maps.importLibrary( + 'maps' + )) as google.maps.MapsLibrary; + + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false + }); + + infoWindow = new InfoWindow(); + getPlaceDetails(); +} + +async function getPlaceDetails() { + // Request needed libraries. + const [ {AdvancedMarkerElement}, { Place } ] = await Promise.all([ + google.maps.importLibrary('marker') as Promise, + google.maps.importLibrary('places') as Promise, + ]); + + // [START maps_ai_powered_summaries_basic_placeid] + // Use place ID to create a new Place instance. + const place = new Place({ + id: 'ChIJzzc-aWUM3IARPOQr9sA6vfY', // San Diego Botanic Garden + }); + // [END maps_ai_powered_summaries_basic_placeid] + + // Call fetchFields, passing the needed data fields. + // [START maps_ai_powered_summaries_basic_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + ], + }); + // [END maps_ai_powered_summaries_basic_fetchfields] + + // Add an Advanced Marker + const marker = new AdvancedMarkerElement({ + map: innerMap, + position: place.location, + title: place.displayName, + }); + + // Create a content container. + const content = document.createElement('div'); + // Populate the container with data. + const address = document.createElement('div'); + const summary = document.createElement('div'); + const lineBreak = document.createElement('br'); + const attribution = document.createElement('div'); + + // Retrieve the textual data (summary, disclosure, flag URI). + //@ts-ignore + let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; + //@ts-ignore + let disclosureText = place.generativeSummary.disclosureText; + //@ts-ignore + let reportingUri = place.generativeSummary.flagContentURI; + + // Create HTML for reporting link. + const reportingLink = document.createElement('a'); + reportingLink.href = reportingUri; + reportingLink.target = '_blank'; + reportingLink.textContent = "Report a problem." + + // Add text to layout. + address.textContent = place.formattedAddress ?? ''; + summary.textContent = overviewText; + attribution.textContent = `${disclosureText} `; + attribution.appendChild(reportingLink); + + content.append(address, lineBreak, summary, lineBreak, attribution); + + innerMap.setCenter(place.location); + + // Handle marker click. + marker.addListener('gmp-click', () => { + showInfoWindow(marker, place, content); + }); + + // Display the info window at load time. + showInfoWindow(marker, place, content); +} + +function showInfoWindow(marker, place, content) { + // Display an info window. + infoWindow.setHeaderContent(place.displayName); + infoWindow.setContent(content); + infoWindow.open({ + anchor: marker, + }); +} + +initMap(); +// [END maps_ai_powered_summaries_basic] diff --git a/dist/samples/ai-powered-summaries-basic/docs/style.css b/dist/samples/ai-powered-summaries-basic/docs/style.css new file mode 100644 index 00000000..93167d23 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/docs/style.css @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries_basic] */ +/* + * Always set the map height explicitly to define the size of the div element + * that contains the map. + */ +#map { + height: 100%; +} + +/* + * Optional: Makes the sample page fill the window. + */ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* [END maps_ai_powered_summaries_basic] */ diff --git a/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.css b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.css new file mode 100644 index 00000000..effa9067 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.css @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Always set the map height explicitly to define the size of the div element + * that contains the map. + */ +#map { + height: 100%; +} + +/* + * Optional: Makes the sample page fill the window. + */ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + + diff --git a/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.details b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.details new file mode 100644 index 00000000..b6e86226 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.details @@ -0,0 +1,7 @@ +name: ai-powered-summaries-basic +authors: + - Geo Developer IX Documentation Team +tags: + - google maps +load_type: h +description: Sample code supporting Google Maps Platform JavaScript API documentation. diff --git a/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.html b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.html new file mode 100644 index 00000000..5f485c7c --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.html @@ -0,0 +1,26 @@ + + + + + + AI-powered Summaries Basic Sample + + + + + + + + + + + + diff --git a/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.js b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.js new file mode 100644 index 00000000..83b3ffa2 --- /dev/null +++ b/dist/samples/ai-powered-summaries-basic/jsfiddle/demo.js @@ -0,0 +1,91 @@ +"use strict"; +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +const mapElement = document.querySelector('gmp-map'); +let innerMap; +let infoWindow; +async function initMap() { + const { Map, InfoWindow } = (await google.maps.importLibrary('maps')); + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false + }); + infoWindow = new InfoWindow(); + getPlaceDetails(); +} +async function getPlaceDetails() { + // Request needed libraries. + const [{ AdvancedMarkerElement }, { Place }] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + + // Use place ID to create a new Place instance. + const place = new Place({ + id: 'ChIJzzc-aWUM3IARPOQr9sA6vfY', // San Diego Botanic Garden + }); + + // Call fetchFields, passing the needed data fields. + + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + ], + }); + + // Add an Advanced Marker + const marker = new AdvancedMarkerElement({ + map: innerMap, + position: place.location, + title: place.displayName, + }); + // Create a content container. + const content = document.createElement('div'); + // Populate the container with data. + const address = document.createElement('div'); + const summary = document.createElement('div'); + const lineBreak = document.createElement('br'); + const attribution = document.createElement('div'); + // Retrieve the textual data (summary, disclosure, flag URI). + //@ts-ignore + let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; + //@ts-ignore + let disclosureText = place.generativeSummary.disclosureText; + //@ts-ignore + let reportingUri = place.generativeSummary.flagContentURI; + // Create HTML for reporting link. + const reportingLink = document.createElement('a'); + reportingLink.href = reportingUri; + reportingLink.target = '_blank'; + reportingLink.textContent = "Report a problem."; + // Add text to layout. + address.textContent = place.formattedAddress ?? ''; + summary.textContent = overviewText; + attribution.textContent = `${disclosureText} `; + attribution.appendChild(reportingLink); + content.append(address, lineBreak, summary, lineBreak, attribution); + innerMap.setCenter(place.location); + // Handle marker click. + marker.addListener('gmp-click', () => { + showInfoWindow(marker, place, content); + }); + // Display the info window at load time. + showInfoWindow(marker, place, content); +} +function showInfoWindow(marker, place, content) { + // Display an info window. + infoWindow.setHeaderContent(place.displayName); + infoWindow.setContent(content); + infoWindow.open({ + anchor: marker, + }); +} +initMap(); + diff --git a/dist/samples/ai-powered-summaries/app/.eslintsrc.json b/dist/samples/ai-powered-summaries/app/.eslintsrc.json new file mode 100644 index 00000000..4c44dab0 --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/.eslintsrc.json @@ -0,0 +1,13 @@ +{ + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "rules": { + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-this-alias": 1, + "@typescript-eslint/no-empty-function": 1, + "@typescript-eslint/explicit-module-boundary-types": 1, + "@typescript-eslint/no-unused-vars": 1 + } +} diff --git a/dist/samples/ai-powered-summaries/app/README.md b/dist/samples/ai-powered-summaries/app/README.md new file mode 100644 index 00000000..709d9585 --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/README.md @@ -0,0 +1,36 @@ +# Google Maps JavaScript Sample + +## ai-powered-summaries + +The ai-powered-summaries sample demonstrates how to show AI-powered summaries a map. + +Follow these instructions to set up and run ai-powered-summaries sample on your local computer. + +## Setup + +### Before starting run: + +`npm i` + +### Run an example on a local web server + +First `cd` to the folder for the sample to run, then: + +`npm start` + +### Build an individual example + +From `samples/`: + +`npm run build --workspace=ai-powered-summaries/` + +### Build all of the examples. + +From `samples/`: + +`npm run build-all` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). diff --git a/dist/samples/ai-powered-summaries/app/index.html b/dist/samples/ai-powered-summaries/app/index.html new file mode 100644 index 00000000..02da9252 --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/index.html @@ -0,0 +1,52 @@ + + + + + + AI Place Summaries + + + + + + + + +
    +

    Search for a place with AI summaries:

    + +
    + + + +
    + + + diff --git a/dist/samples/ai-powered-summaries/app/index.ts b/dist/samples/ai-powered-summaries/app/index.ts new file mode 100644 index 00000000..4c44a671 --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/index.ts @@ -0,0 +1,257 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_ai_powered_summaries] +// Define DOM elements. +const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; +const placeAutocomplete = document.querySelector( + 'gmp-place-autocomplete' +) as google.maps.places.PlaceAutocompleteElement; +const summaryPanel = document.getElementById('summary-panel') as HTMLDivElement; +const placeName = document.getElementById('place-name') as HTMLElement; +const placeAddress = document.getElementById('place-address') as HTMLElement; +const tabContainer = document.getElementById('tab-container') as HTMLDivElement; +const summaryContent = document.getElementById( + 'summary-content' +) as HTMLDivElement; +const aiDisclosure = document.getElementById('ai-disclosure') as HTMLDivElement; +const flagContentLink = document.getElementById('flag-content-link') as HTMLAnchorElement; + +let innerMap; +let marker: google.maps.marker.AdvancedMarkerElement; + +async function initMap(): Promise { + // Request needed libraries. + const [] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + }); + + // Bind autocomplete bounds to map bounds. + google.maps.event.addListener(innerMap, 'bounds_changed', async () => { + placeAutocomplete.locationRestriction = innerMap.getBounds(); + }); + + // Create the marker. + marker = new google.maps.marker.AdvancedMarkerElement({ + map: innerMap, + }); + + // Handle selection of an autocomplete result. + // prettier-ignore + // @ts-ignore + placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { + const place = placePrediction.toPlace(); + + // Fetch all summary fields. + // [START maps_ai_powered_summaries_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + 'neighborhoodSummary', + 'reviewSummary', + 'evChargeAmenitySummary', + ], + }); + // [END maps_ai_powered_summaries_fetchfields] + + // Update the map viewport and position the marker. + if (place.viewport) { + innerMap.fitBounds(place.viewport); + } else { + innerMap.setCenter(place.location); + innerMap.setZoom(17); + } + marker.position = place.location; + + // Update the panel UI. + updateSummaryPanel(place); + } + ); +} + +function updateSummaryPanel(place: google.maps.places.Place) { + // Reset UI + summaryPanel.classList.remove('hidden'); + tabContainer.innerHTML = ''; // innerHTML is OK here since we're clearing known child elements. + summaryContent.textContent = ''; + aiDisclosure.textContent = ''; + + placeName.textContent = place.displayName || ''; + placeAddress.textContent = place.formattedAddress || ''; + + let firstTabActivated = false; + + /** + * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). + */ + const createTab = ( + label: string, + content: string | Node, + disclosure: string, + flagUrl: string + ) => { + const btn = document.createElement('button'); + btn.className = 'tab-button'; + btn.textContent = label; + + btn.onclick = () => { + // Do nothing if the tab is already active. + if (btn.classList.contains('active')) { + return; + } + + // Manage the active class state. + document + .querySelectorAll('.tab-button') + .forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + + if (typeof content === 'string') { + summaryContent.textContent = content; + } else { + summaryContent.replaceChildren(content.cloneNode(true)); + } + + // Set the disclosure text. + aiDisclosure.textContent = disclosure || 'AI-generated content.'; + + // Add the content flag URI. + if (flagUrl) { + flagContentLink.href = flagUrl; + flagContentLink.textContent = "Report an issue" + } + }; + + tabContainer.appendChild(btn); + + // Auto-select the first available summary. + if (!firstTabActivated) { + btn.click(); + firstTabActivated = true; + } + }; + + // --- 1. Generative Summary (Place) --- + //@ts-ignore + if (place.generativeSummary?.overview) { + createTab( + 'Overview', + //@ts-ignore + place.generativeSummary.overview, + //@ts-ignore + place.generativeSummary.disclosureText, + //@ts-ignore + place.generativeSummary.flagContentURI + ); + } + + // --- 2. Review Summary --- + //@ts-ignore + if (place.reviewSummary?.text) { + createTab( + 'Reviews', + //@ts-ignore + place.reviewSummary.text, + //@ts-ignore + place.reviewSummary.disclosureText, + //@ts-ignore + place.reviewSummary.flagContentURI + ); + } + + // --- 3. Neighborhood Summary --- + //@ts-ignore + if (place.neighborhoodSummary?.overview?.content) { + createTab( + 'Neighborhood', + //@ts-ignore + place.neighborhoodSummary.overview.content, + //@ts-ignore + place.neighborhoodSummary.disclosureText, + //@ts-ignore + place.neighborhoodSummary.flagContentURI + ); + } + + // --- 4. EV Amenity Summary (uses content blocks)) --- + //@ts-ignore + if (place.evChargeAmenitySummary) { + //@ts-ignore + const evSummary = place.evChargeAmenitySummary; + const evContainer = document.createDocumentFragment(); + + // Helper to build a safe DOM section for EV categories. + const createSection = (title: string, text: string) => { + const wrapper = document.createElement('div'); + wrapper.style.marginBottom = '15px'; // Or use a CSS class + + const titleEl = document.createElement('strong'); + titleEl.textContent = title; + + const textEl = document.createElement('div'); + textEl.textContent = text; + + wrapper.appendChild(titleEl); + wrapper.appendChild(textEl); + return wrapper; + }; + + // Check and append each potential section + if (evSummary.overview?.content) { + evContainer.appendChild( + createSection('Overview', evSummary.overview.content) + ); + } + if (evSummary.coffee?.content) { + evContainer.appendChild( + createSection('Coffee', evSummary.coffee.content) + ); + } + if (evSummary.restaurant?.content) { + evContainer.appendChild( + createSection('Food', evSummary.restaurant.content) + ); + } + if (evSummary.store?.content) { + evContainer.appendChild( + createSection('Shopping', evSummary.store.content) + ); + } + + // Only add the tab if the container has children + if (evContainer.hasChildNodes()) { + createTab( + 'EV Amenities', + evContainer, // Passing a Node instead of string + evSummary.disclosureText, + evSummary.flagContentURI + ); + } + } + + // Safely handle the empty state. + if (!firstTabActivated) { + const msg = document.createElement('em'); + msg.textContent = + 'No AI summaries are available for this specific location.'; + summaryContent.replaceChildren(msg); + aiDisclosure.textContent = ''; + } +} + +initMap(); +// [END maps_ai_powered_summaries] diff --git a/dist/samples/ai-powered-summaries/app/package.json b/dist/samples/ai-powered-summaries/app/package.json new file mode 100644 index 00000000..e9df893e --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/package.json @@ -0,0 +1,14 @@ +{ + "name": "@js-api-samples/ai-powered-summaries", + "version": "1.0.0", + "scripts": { + "build": "tsc && bash ../jsfiddle.sh ai-powered-summaries && bash ../app.sh ai-powered-summaries && bash ../docs.sh ai-powered-summaries && npm run build:vite --workspace=. && bash ../dist.sh ai-powered-summaries", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --base './' && vite", + "build:vite": "vite build --base './'", + "preview": "vite preview" + }, + "dependencies": { + + } +} diff --git a/dist/samples/ai-powered-summaries/app/style.css b/dist/samples/ai-powered-summaries/app/style.css new file mode 100644 index 00000000..31092e63 --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/style.css @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries] */ +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; +} +/* [END maps_ai_powered_summaries] */ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries] */ +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; +} +/* [END maps_ai_powered_summaries] */ diff --git a/dist/samples/ai-powered-summaries/app/tsconfig.json b/dist/samples/ai-powered-summaries/app/tsconfig.json new file mode 100644 index 00000000..366aabb0 --- /dev/null +++ b/dist/samples/ai-powered-summaries/app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true, + "noImplicitAny": false, + "lib": [ + "es2015", + "esnext", + "es6", + "dom", + "dom.iterable" + ], + "moduleResolution": "Node", + "jsx": "preserve" + } +} diff --git a/dist/samples/ai-powered-summaries/dist/assets/index-D-fh7R-p.js b/dist/samples/ai-powered-summaries/dist/assets/index-D-fh7R-p.js new file mode 100644 index 00000000..0ebbd6be --- /dev/null +++ b/dist/samples/ai-powered-summaries/dist/assets/index-D-fh7R-p.js @@ -0,0 +1 @@ +(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))n(e);new MutationObserver(e=>{for(const o of e)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&n(a)}).observe(document,{childList:!0,subtree:!0});function c(e){const o={};return e.integrity&&(o.integrity=e.integrity),e.referrerPolicy&&(o.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?o.credentials="include":e.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function n(e){if(e.ep)return;e.ep=!0;const o=c(e);fetch(e.href,o)}})();const h=document.querySelector("gmp-map"),p=document.querySelector("gmp-place-autocomplete"),C=document.getElementById("summary-panel"),S=document.getElementById("place-name"),b=document.getElementById("place-address"),y=document.getElementById("tab-container"),l=document.getElementById("summary-content"),d=document.getElementById("ai-disclosure"),g=document.getElementById("flag-content-link");let s,v;async function w(){await Promise.all([google.maps.importLibrary("marker"),google.maps.importLibrary("places")]),s=h.innerMap,s.setOptions({mapTypeControl:!1,streetViewControl:!1,fullscreenControl:!1}),google.maps.event.addListener(s,"bounds_changed",async()=>{p.locationRestriction=s.getBounds()}),v=new google.maps.marker.AdvancedMarkerElement({map:s}),p.addEventListener("gmp-select",async({placePrediction:t})=>{const r=t.toPlace();await r.fetchFields({fields:["displayName","formattedAddress","location","generativeSummary","neighborhoodSummary","reviewSummary","evChargeAmenitySummary"]}),r.viewport?s.fitBounds(r.viewport):(s.setCenter(r.location),s.setZoom(17)),v.position=r.location,x(r)})}function x(t){C.classList.remove("hidden"),y.innerHTML="",l.textContent="",d.textContent="",S.textContent=t.displayName||"",b.textContent=t.formattedAddress||"";let r=!1;const c=(n,e,o,a)=>{const i=document.createElement("button");i.className="tab-button",i.textContent=n,i.onclick=()=>{i.classList.contains("active")||(document.querySelectorAll(".tab-button").forEach(m=>m.classList.remove("active")),i.classList.add("active"),typeof e=="string"?l.textContent=e:l.replaceChildren(e.cloneNode(!0)),d.textContent=o||"AI-generated content.",a&&(g.href=a,g.textContent="Report an issue"))},y.appendChild(i),r||(i.click(),r=!0)};if(t.generativeSummary?.overview&&c("Overview",t.generativeSummary.overview,t.generativeSummary.disclosureText,t.generativeSummary.flagContentURI),t.reviewSummary?.text&&c("Reviews",t.reviewSummary.text,t.reviewSummary.disclosureText,t.reviewSummary.flagContentURI),t.neighborhoodSummary?.overview?.content&&c("Neighborhood",t.neighborhoodSummary.overview.content,t.neighborhoodSummary.disclosureText,t.neighborhoodSummary.flagContentURI),t.evChargeAmenitySummary){const n=t.evChargeAmenitySummary,e=document.createDocumentFragment(),o=(a,i)=>{const m=document.createElement("div");m.style.marginBottom="15px";const u=document.createElement("strong");u.textContent=a;const f=document.createElement("div");return f.textContent=i,m.appendChild(u),m.appendChild(f),m};n.overview?.content&&e.appendChild(o("Overview",n.overview.content)),n.coffee?.content&&e.appendChild(o("Coffee",n.coffee.content)),n.restaurant?.content&&e.appendChild(o("Food",n.restaurant.content)),n.store?.content&&e.appendChild(o("Shopping",n.store.content)),e.hasChildNodes()&&c("EV Amenities",e,n.disclosureText,n.flagContentURI)}if(!r){const n=document.createElement("em");n.textContent="No AI summaries are available for this specific location.",l.replaceChildren(n),d.textContent=""}}w(); diff --git a/dist/samples/ai-powered-summaries/dist/assets/index-qoXx_FcE.css b/dist/samples/ai-powered-summaries/dist/assets/index-qoXx_FcE.css new file mode 100644 index 00000000..c41b9d46 --- /dev/null +++ b/dist/samples/ai-powered-summaries/dist/assets/index-qoXx_FcE.css @@ -0,0 +1 @@ +.flag-content-link{font-size:.75rem;color:#666;padding:10px 15px;border-top:1px solid #eee}gmp-map{height:100%}html,body{height:100%;margin:0;padding:0}.place-autocomplete-card{background-color:#fff;border-radius:5px;box-shadow:#00000059 0 5px 15px;margin:10px;padding:15px;font-family:Roboto,sans-serif;font-size:1rem}gmp-place-autocomplete{width:300px}.summary-card{background-color:#fff;border-radius:5px;box-shadow:#00000059 0 5px 15px;margin:10px;padding:0;font-family:Roboto,sans-serif;width:350px;max-height:80vh;overflow-y:auto;display:flex;flex-direction:column}.hidden{display:none}#place-header{padding:15px;background-color:#f8f9fa;border-bottom:1px solid #ddd}#place-header h2{margin:0 0 5px;font-size:1.2rem}#place-address{margin:0;color:#555;font-size:.9rem}.tab-container{display:flex;border-bottom:1px solid #ddd;background-color:#fff}.tab-button{flex:1;background:none;border:none;padding:10px;cursor:pointer;font-weight:500;color:#555;border-bottom:3px solid transparent}.tab-button:hover{background-color:#f1f1f1}.tab-button.active{font-weight:700;border-bottom:3px solid #000000}.tab-button.active:hover{background-color:#fff;cursor:default}.content-area{padding:15px;line-height:1.5;font-size:.95rem;color:#333}.disclosure-footer{font-size:.75rem;color:#666;padding:10px 15px;border-top:1px solid #eee;font-style:italic}.flag-content-link{font-size:.75rem;color:#666;padding:10px 15px} diff --git a/dist/samples/ai-powered-summaries/dist/index.html b/dist/samples/ai-powered-summaries/dist/index.html new file mode 100644 index 00000000..670700e2 --- /dev/null +++ b/dist/samples/ai-powered-summaries/dist/index.html @@ -0,0 +1,52 @@ + + + + + + AI Place Summaries + + + + + + + + +
    +

    Search for a place with AI summaries:

    + +
    + + + +
    + + + diff --git a/dist/samples/ai-powered-summaries/docs/index.html b/dist/samples/ai-powered-summaries/docs/index.html new file mode 100644 index 00000000..02da9252 --- /dev/null +++ b/dist/samples/ai-powered-summaries/docs/index.html @@ -0,0 +1,52 @@ + + + + + + AI Place Summaries + + + + + + + + +
    +

    Search for a place with AI summaries:

    + +
    + + + +
    + + + diff --git a/dist/samples/ai-powered-summaries/docs/index.js b/dist/samples/ai-powered-summaries/docs/index.js new file mode 100644 index 00000000..651715d3 --- /dev/null +++ b/dist/samples/ai-powered-summaries/docs/index.js @@ -0,0 +1,199 @@ +"use strict"; +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// [START maps_ai_powered_summaries] +// Define DOM elements. +const mapElement = document.querySelector('gmp-map'); +const placeAutocomplete = document.querySelector('gmp-place-autocomplete'); +const summaryPanel = document.getElementById('summary-panel'); +const placeName = document.getElementById('place-name'); +const placeAddress = document.getElementById('place-address'); +const tabContainer = document.getElementById('tab-container'); +const summaryContent = document.getElementById('summary-content'); +const aiDisclosure = document.getElementById('ai-disclosure'); +const flagContentLink = document.getElementById('flag-content-link'); +let innerMap; +let marker; +async function initMap() { + // Request needed libraries. + const [] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + }); + // Bind autocomplete bounds to map bounds. + google.maps.event.addListener(innerMap, 'bounds_changed', async () => { + placeAutocomplete.locationRestriction = innerMap.getBounds(); + }); + // Create the marker. + marker = new google.maps.marker.AdvancedMarkerElement({ + map: innerMap, + }); + // Handle selection of an autocomplete result. + // prettier-ignore + // @ts-ignore + placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { + const place = placePrediction.toPlace(); + // Fetch all summary fields. + // [START maps_ai_powered_summaries_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + 'neighborhoodSummary', + 'reviewSummary', + 'evChargeAmenitySummary', + ], + }); + // [END maps_ai_powered_summaries_fetchfields] + // Update the map viewport and position the marker. + if (place.viewport) { + innerMap.fitBounds(place.viewport); + } + else { + innerMap.setCenter(place.location); + innerMap.setZoom(17); + } + marker.position = place.location; + // Update the panel UI. + updateSummaryPanel(place); + }); +} +function updateSummaryPanel(place) { + // Reset UI + summaryPanel.classList.remove('hidden'); + tabContainer.innerHTML = ''; // innerHTML is OK here since we're clearing known child elements. + summaryContent.textContent = ''; + aiDisclosure.textContent = ''; + placeName.textContent = place.displayName || ''; + placeAddress.textContent = place.formattedAddress || ''; + let firstTabActivated = false; + /** + * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). + */ + const createTab = (label, content, disclosure, flagUrl) => { + const btn = document.createElement('button'); + btn.className = 'tab-button'; + btn.textContent = label; + btn.onclick = () => { + // Do nothing if the tab is already active. + if (btn.classList.contains('active')) { + return; + } + // Manage the active class state. + document + .querySelectorAll('.tab-button') + .forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + if (typeof content === 'string') { + summaryContent.textContent = content; + } + else { + summaryContent.replaceChildren(content.cloneNode(true)); + } + // Set the disclosure text. + aiDisclosure.textContent = disclosure || 'AI-generated content.'; + // Add the content flag URI. + if (flagUrl) { + flagContentLink.href = flagUrl; + flagContentLink.textContent = "Report an issue"; + } + }; + tabContainer.appendChild(btn); + // Auto-select the first available summary. + if (!firstTabActivated) { + btn.click(); + firstTabActivated = true; + } + }; + // --- 1. Generative Summary (Place) --- + //@ts-ignore + if (place.generativeSummary?.overview) { + createTab('Overview', + //@ts-ignore + place.generativeSummary.overview, + //@ts-ignore + place.generativeSummary.disclosureText, + //@ts-ignore + place.generativeSummary.flagContentURI); + } + // --- 2. Review Summary --- + //@ts-ignore + if (place.reviewSummary?.text) { + createTab('Reviews', + //@ts-ignore + place.reviewSummary.text, + //@ts-ignore + place.reviewSummary.disclosureText, + //@ts-ignore + place.reviewSummary.flagContentURI); + } + // --- 3. Neighborhood Summary --- + //@ts-ignore + if (place.neighborhoodSummary?.overview?.content) { + createTab('Neighborhood', + //@ts-ignore + place.neighborhoodSummary.overview.content, + //@ts-ignore + place.neighborhoodSummary.disclosureText, + //@ts-ignore + place.neighborhoodSummary.flagContentURI); + } + // --- 4. EV Amenity Summary (uses content blocks)) --- + //@ts-ignore + if (place.evChargeAmenitySummary) { + //@ts-ignore + const evSummary = place.evChargeAmenitySummary; + const evContainer = document.createDocumentFragment(); + // Helper to build a safe DOM section for EV categories. + const createSection = (title, text) => { + const wrapper = document.createElement('div'); + wrapper.style.marginBottom = '15px'; // Or use a CSS class + const titleEl = document.createElement('strong'); + titleEl.textContent = title; + const textEl = document.createElement('div'); + textEl.textContent = text; + wrapper.appendChild(titleEl); + wrapper.appendChild(textEl); + return wrapper; + }; + // Check and append each potential section + if (evSummary.overview?.content) { + evContainer.appendChild(createSection('Overview', evSummary.overview.content)); + } + if (evSummary.coffee?.content) { + evContainer.appendChild(createSection('Coffee', evSummary.coffee.content)); + } + if (evSummary.restaurant?.content) { + evContainer.appendChild(createSection('Food', evSummary.restaurant.content)); + } + if (evSummary.store?.content) { + evContainer.appendChild(createSection('Shopping', evSummary.store.content)); + } + // Only add the tab if the container has children + if (evContainer.hasChildNodes()) { + createTab('EV Amenities', evContainer, // Passing a Node instead of string + evSummary.disclosureText, evSummary.flagContentURI); + } + } + // Safely handle the empty state. + if (!firstTabActivated) { + const msg = document.createElement('em'); + msg.textContent = + 'No AI summaries are available for this specific location.'; + summaryContent.replaceChildren(msg); + aiDisclosure.textContent = ''; + } +} +initMap(); +// [END maps_ai_powered_summaries] diff --git a/dist/samples/ai-powered-summaries/docs/index.ts b/dist/samples/ai-powered-summaries/docs/index.ts new file mode 100644 index 00000000..4c44a671 --- /dev/null +++ b/dist/samples/ai-powered-summaries/docs/index.ts @@ -0,0 +1,257 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_ai_powered_summaries] +// Define DOM elements. +const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; +const placeAutocomplete = document.querySelector( + 'gmp-place-autocomplete' +) as google.maps.places.PlaceAutocompleteElement; +const summaryPanel = document.getElementById('summary-panel') as HTMLDivElement; +const placeName = document.getElementById('place-name') as HTMLElement; +const placeAddress = document.getElementById('place-address') as HTMLElement; +const tabContainer = document.getElementById('tab-container') as HTMLDivElement; +const summaryContent = document.getElementById( + 'summary-content' +) as HTMLDivElement; +const aiDisclosure = document.getElementById('ai-disclosure') as HTMLDivElement; +const flagContentLink = document.getElementById('flag-content-link') as HTMLAnchorElement; + +let innerMap; +let marker: google.maps.marker.AdvancedMarkerElement; + +async function initMap(): Promise { + // Request needed libraries. + const [] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + }); + + // Bind autocomplete bounds to map bounds. + google.maps.event.addListener(innerMap, 'bounds_changed', async () => { + placeAutocomplete.locationRestriction = innerMap.getBounds(); + }); + + // Create the marker. + marker = new google.maps.marker.AdvancedMarkerElement({ + map: innerMap, + }); + + // Handle selection of an autocomplete result. + // prettier-ignore + // @ts-ignore + placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { + const place = placePrediction.toPlace(); + + // Fetch all summary fields. + // [START maps_ai_powered_summaries_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + 'neighborhoodSummary', + 'reviewSummary', + 'evChargeAmenitySummary', + ], + }); + // [END maps_ai_powered_summaries_fetchfields] + + // Update the map viewport and position the marker. + if (place.viewport) { + innerMap.fitBounds(place.viewport); + } else { + innerMap.setCenter(place.location); + innerMap.setZoom(17); + } + marker.position = place.location; + + // Update the panel UI. + updateSummaryPanel(place); + } + ); +} + +function updateSummaryPanel(place: google.maps.places.Place) { + // Reset UI + summaryPanel.classList.remove('hidden'); + tabContainer.innerHTML = ''; // innerHTML is OK here since we're clearing known child elements. + summaryContent.textContent = ''; + aiDisclosure.textContent = ''; + + placeName.textContent = place.displayName || ''; + placeAddress.textContent = place.formattedAddress || ''; + + let firstTabActivated = false; + + /** + * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). + */ + const createTab = ( + label: string, + content: string | Node, + disclosure: string, + flagUrl: string + ) => { + const btn = document.createElement('button'); + btn.className = 'tab-button'; + btn.textContent = label; + + btn.onclick = () => { + // Do nothing if the tab is already active. + if (btn.classList.contains('active')) { + return; + } + + // Manage the active class state. + document + .querySelectorAll('.tab-button') + .forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + + if (typeof content === 'string') { + summaryContent.textContent = content; + } else { + summaryContent.replaceChildren(content.cloneNode(true)); + } + + // Set the disclosure text. + aiDisclosure.textContent = disclosure || 'AI-generated content.'; + + // Add the content flag URI. + if (flagUrl) { + flagContentLink.href = flagUrl; + flagContentLink.textContent = "Report an issue" + } + }; + + tabContainer.appendChild(btn); + + // Auto-select the first available summary. + if (!firstTabActivated) { + btn.click(); + firstTabActivated = true; + } + }; + + // --- 1. Generative Summary (Place) --- + //@ts-ignore + if (place.generativeSummary?.overview) { + createTab( + 'Overview', + //@ts-ignore + place.generativeSummary.overview, + //@ts-ignore + place.generativeSummary.disclosureText, + //@ts-ignore + place.generativeSummary.flagContentURI + ); + } + + // --- 2. Review Summary --- + //@ts-ignore + if (place.reviewSummary?.text) { + createTab( + 'Reviews', + //@ts-ignore + place.reviewSummary.text, + //@ts-ignore + place.reviewSummary.disclosureText, + //@ts-ignore + place.reviewSummary.flagContentURI + ); + } + + // --- 3. Neighborhood Summary --- + //@ts-ignore + if (place.neighborhoodSummary?.overview?.content) { + createTab( + 'Neighborhood', + //@ts-ignore + place.neighborhoodSummary.overview.content, + //@ts-ignore + place.neighborhoodSummary.disclosureText, + //@ts-ignore + place.neighborhoodSummary.flagContentURI + ); + } + + // --- 4. EV Amenity Summary (uses content blocks)) --- + //@ts-ignore + if (place.evChargeAmenitySummary) { + //@ts-ignore + const evSummary = place.evChargeAmenitySummary; + const evContainer = document.createDocumentFragment(); + + // Helper to build a safe DOM section for EV categories. + const createSection = (title: string, text: string) => { + const wrapper = document.createElement('div'); + wrapper.style.marginBottom = '15px'; // Or use a CSS class + + const titleEl = document.createElement('strong'); + titleEl.textContent = title; + + const textEl = document.createElement('div'); + textEl.textContent = text; + + wrapper.appendChild(titleEl); + wrapper.appendChild(textEl); + return wrapper; + }; + + // Check and append each potential section + if (evSummary.overview?.content) { + evContainer.appendChild( + createSection('Overview', evSummary.overview.content) + ); + } + if (evSummary.coffee?.content) { + evContainer.appendChild( + createSection('Coffee', evSummary.coffee.content) + ); + } + if (evSummary.restaurant?.content) { + evContainer.appendChild( + createSection('Food', evSummary.restaurant.content) + ); + } + if (evSummary.store?.content) { + evContainer.appendChild( + createSection('Shopping', evSummary.store.content) + ); + } + + // Only add the tab if the container has children + if (evContainer.hasChildNodes()) { + createTab( + 'EV Amenities', + evContainer, // Passing a Node instead of string + evSummary.disclosureText, + evSummary.flagContentURI + ); + } + } + + // Safely handle the empty state. + if (!firstTabActivated) { + const msg = document.createElement('em'); + msg.textContent = + 'No AI summaries are available for this specific location.'; + summaryContent.replaceChildren(msg); + aiDisclosure.textContent = ''; + } +} + +initMap(); +// [END maps_ai_powered_summaries] diff --git a/dist/samples/ai-powered-summaries/docs/style.css b/dist/samples/ai-powered-summaries/docs/style.css new file mode 100644 index 00000000..31092e63 --- /dev/null +++ b/dist/samples/ai-powered-summaries/docs/style.css @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries] */ +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; +} +/* [END maps_ai_powered_summaries] */ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries] */ +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; +} +/* [END maps_ai_powered_summaries] */ diff --git a/dist/samples/ai-powered-summaries/jsfiddle/demo.css b/dist/samples/ai-powered-summaries/jsfiddle/demo.css new file mode 100644 index 00000000..1525097d --- /dev/null +++ b/dist/samples/ai-powered-summaries/jsfiddle/demo.css @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; +} + +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; +} + diff --git a/dist/samples/ai-powered-summaries/jsfiddle/demo.details b/dist/samples/ai-powered-summaries/jsfiddle/demo.details new file mode 100644 index 00000000..22b05589 --- /dev/null +++ b/dist/samples/ai-powered-summaries/jsfiddle/demo.details @@ -0,0 +1,7 @@ +name: ai-powered-summaries +authors: + - Geo Developer IX Documentation Team +tags: + - google maps +load_type: h +description: Sample code supporting Google Maps Platform JavaScript API documentation. diff --git a/dist/samples/ai-powered-summaries/jsfiddle/demo.html b/dist/samples/ai-powered-summaries/jsfiddle/demo.html new file mode 100644 index 00000000..34258a6c --- /dev/null +++ b/dist/samples/ai-powered-summaries/jsfiddle/demo.html @@ -0,0 +1,52 @@ + + + + + + AI Place Summaries + + + + + + + + +
    +

    Search for a place with AI summaries:

    + +
    + + + +
    + + + diff --git a/dist/samples/ai-powered-summaries/jsfiddle/demo.js b/dist/samples/ai-powered-summaries/jsfiddle/demo.js new file mode 100644 index 00000000..070184e4 --- /dev/null +++ b/dist/samples/ai-powered-summaries/jsfiddle/demo.js @@ -0,0 +1,199 @@ +"use strict"; +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Define DOM elements. +const mapElement = document.querySelector('gmp-map'); +const placeAutocomplete = document.querySelector('gmp-place-autocomplete'); +const summaryPanel = document.getElementById('summary-panel'); +const placeName = document.getElementById('place-name'); +const placeAddress = document.getElementById('place-address'); +const tabContainer = document.getElementById('tab-container'); +const summaryContent = document.getElementById('summary-content'); +const aiDisclosure = document.getElementById('ai-disclosure'); +const flagContentLink = document.getElementById('flag-content-link'); +let innerMap; +let marker; +async function initMap() { + // Request needed libraries. + const [] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + }); + // Bind autocomplete bounds to map bounds. + google.maps.event.addListener(innerMap, 'bounds_changed', async () => { + placeAutocomplete.locationRestriction = innerMap.getBounds(); + }); + // Create the marker. + marker = new google.maps.marker.AdvancedMarkerElement({ + map: innerMap, + }); + // Handle selection of an autocomplete result. + // prettier-ignore + // @ts-ignore + placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { + const place = placePrediction.toPlace(); + // Fetch all summary fields. + + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + 'neighborhoodSummary', + 'reviewSummary', + 'evChargeAmenitySummary', + ], + }); + + // Update the map viewport and position the marker. + if (place.viewport) { + innerMap.fitBounds(place.viewport); + } + else { + innerMap.setCenter(place.location); + innerMap.setZoom(17); + } + marker.position = place.location; + // Update the panel UI. + updateSummaryPanel(place); + }); +} +function updateSummaryPanel(place) { + // Reset UI + summaryPanel.classList.remove('hidden'); + tabContainer.innerHTML = ''; // innerHTML is OK here since we're clearing known child elements. + summaryContent.textContent = ''; + aiDisclosure.textContent = ''; + placeName.textContent = place.displayName || ''; + placeAddress.textContent = place.formattedAddress || ''; + let firstTabActivated = false; + /** + * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). + */ + const createTab = (label, content, disclosure, flagUrl) => { + const btn = document.createElement('button'); + btn.className = 'tab-button'; + btn.textContent = label; + btn.onclick = () => { + // Do nothing if the tab is already active. + if (btn.classList.contains('active')) { + return; + } + // Manage the active class state. + document + .querySelectorAll('.tab-button') + .forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + if (typeof content === 'string') { + summaryContent.textContent = content; + } + else { + summaryContent.replaceChildren(content.cloneNode(true)); + } + // Set the disclosure text. + aiDisclosure.textContent = disclosure || 'AI-generated content.'; + // Add the content flag URI. + if (flagUrl) { + flagContentLink.href = flagUrl; + flagContentLink.textContent = "Report an issue"; + } + }; + tabContainer.appendChild(btn); + // Auto-select the first available summary. + if (!firstTabActivated) { + btn.click(); + firstTabActivated = true; + } + }; + // --- 1. Generative Summary (Place) --- + //@ts-ignore + if (place.generativeSummary?.overview) { + createTab('Overview', + //@ts-ignore + place.generativeSummary.overview, + //@ts-ignore + place.generativeSummary.disclosureText, + //@ts-ignore + place.generativeSummary.flagContentURI); + } + // --- 2. Review Summary --- + //@ts-ignore + if (place.reviewSummary?.text) { + createTab('Reviews', + //@ts-ignore + place.reviewSummary.text, + //@ts-ignore + place.reviewSummary.disclosureText, + //@ts-ignore + place.reviewSummary.flagContentURI); + } + // --- 3. Neighborhood Summary --- + //@ts-ignore + if (place.neighborhoodSummary?.overview?.content) { + createTab('Neighborhood', + //@ts-ignore + place.neighborhoodSummary.overview.content, + //@ts-ignore + place.neighborhoodSummary.disclosureText, + //@ts-ignore + place.neighborhoodSummary.flagContentURI); + } + // --- 4. EV Amenity Summary (uses content blocks)) --- + //@ts-ignore + if (place.evChargeAmenitySummary) { + //@ts-ignore + const evSummary = place.evChargeAmenitySummary; + const evContainer = document.createDocumentFragment(); + // Helper to build a safe DOM section for EV categories. + const createSection = (title, text) => { + const wrapper = document.createElement('div'); + wrapper.style.marginBottom = '15px'; // Or use a CSS class + const titleEl = document.createElement('strong'); + titleEl.textContent = title; + const textEl = document.createElement('div'); + textEl.textContent = text; + wrapper.appendChild(titleEl); + wrapper.appendChild(textEl); + return wrapper; + }; + // Check and append each potential section + if (evSummary.overview?.content) { + evContainer.appendChild(createSection('Overview', evSummary.overview.content)); + } + if (evSummary.coffee?.content) { + evContainer.appendChild(createSection('Coffee', evSummary.coffee.content)); + } + if (evSummary.restaurant?.content) { + evContainer.appendChild(createSection('Food', evSummary.restaurant.content)); + } + if (evSummary.store?.content) { + evContainer.appendChild(createSection('Shopping', evSummary.store.content)); + } + // Only add the tab if the container has children + if (evContainer.hasChildNodes()) { + createTab('EV Amenities', evContainer, // Passing a Node instead of string + evSummary.disclosureText, evSummary.flagContentURI); + } + } + // Safely handle the empty state. + if (!firstTabActivated) { + const msg = document.createElement('em'); + msg.textContent = + 'No AI summaries are available for this specific location.'; + summaryContent.replaceChildren(msg); + aiDisclosure.textContent = ''; + } +} +initMap(); + diff --git a/index.html b/index.html index 02f85042..820cd730 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,8 @@

    Maps JSAPI Samples

  • advanced-markers-html-simple
  • advanced-markers-simple
  • advanced-markers-zoom
  • +
  • ai-powered-summaries
  • +
  • ai-powered-summaries-basic
  • boundaries-choropleth
  • boundaries-click
  • boundaries-simple
  • diff --git a/package-lock.json b/package-lock.json index 8b30b48f..d1de45c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1360,6 +1360,14 @@ "resolved": "samples/advanced-markers-zoom", "link": true }, + "node_modules/@js-api-samples/ai-powered-summaries": { + "resolved": "samples/ai-powered-summaries", + "link": true + }, + "node_modules/@js-api-samples/ai-powered-summaries-basic": { + "resolved": "samples/ai-powered-summaries-basic", + "link": true + }, "node_modules/@js-api-samples/boundaries-choropleth": { "resolved": "samples/boundaries-choropleth", "link": true @@ -8268,6 +8276,14 @@ "name": "@js-api-samples/advanced-markers-zoom", "version": "1.0.0" }, + "samples/ai-powered-summaries": { + "name": "@js-api-samples/ai-powered-summaries", + "version": "1.0.0" + }, + "samples/ai-powered-summaries-basic": { + "name": "@js-api-samples/ai-powered-summaries-basic", + "version": "1.0.0" + }, "samples/boundaries-choropleth": { "name": "@js-api-samples/boundaries-choropleth", "version": "1.0.0"