1. Antes de comenzar
En este codelab, aprenderás a compilar una aplicación de búsqueda local completamente interactiva con el kit de IU de Places de Google Maps Platform.
Requisitos previos
- Un proyecto de Google Cloud con las APIs y las credenciales necesarias configuradas
- Conocimientos básicos de HTML y CSS
- Conocimiento de JavaScript moderno
- Un navegador web moderno, como la versión más reciente de Chrome
- El editor de texto que prefieras
Actividades
- Estructurar una aplicación de mapas con una clase de JavaScript
- Cómo usar componentes web para mostrar un mapa
- Usa el elemento Place Search para realizar y mostrar los resultados de una búsqueda de texto.
- Crea y administra de forma programática marcadores de mapa
AdvancedMarkerElement
personalizados. - Mostrar el elemento Place Details cuando un usuario selecciona una ubicación
- Usa la API de Geocoding para crear una interfaz dinámica y fácil de usar.
Requisitos
- Un proyecto de Google Cloud con la facturación habilitada.
- Una clave de API de Google Maps Platform
- Un ID de mapa
- Se habilitaron las siguientes APIs:
- API de Maps JavaScript
- Kit de IU de Places
- API de Geocoding
2. Prepárate
Para el siguiente paso, deberás habilitar la API de Maps JavaScript, el kit de IU de Places y la API de Geocoding.
Configura Google Maps Platform
Si todavía no tienes una cuenta de Google Cloud Platform y un proyecto con la facturación habilitada, consulta la guía Cómo comenzar a utilizar Google Maps Platform para crear una cuenta de facturación y un proyecto.
- En Cloud Console, haz clic en el menú desplegable del proyecto y selecciona el proyecto que deseas usar para este codelab.
- Habilita las API y los SDK de Google Maps Platform necesarios para este codelab en Google Cloud Marketplace. Para hacerlo, sigue los pasos que se indican en este video o esta documentación.
- Genera una clave de API en la página Credenciales de Cloud Console. Puedes seguir los pasos que se indican en este video o esta documentación. Todas las solicitudes a Google Maps Platform requieren una clave de API.
3. El shell de la aplicación y un mapa funcional
En este primer paso, crearemos el diseño visual completo de nuestra aplicación y estableceremos una estructura limpia basada en clases para nuestro código JavaScript. Esto nos brinda una base sólida sobre la cual construir. Al final de esta sección, tendrás una página con diseño que muestra un mapa interactivo.
Crea el archivo HTML
Primero, crea un archivo llamado index.html
. Este archivo contendrá la estructura completa de nuestra aplicación, incluidos el encabezado, los filtros de búsqueda, la barra lateral, el contenedor del mapa y los componentes web necesarios.
Copia el siguiente código en index.html
. Asegúrate de reemplazar YOUR_API_KEY_HERE
por tu propia clave de API de Google Maps Platform y DEMO_MAP_ID
por tu propio ID de mapa de Google Maps Platform.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Local Search App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Google Fonts: Roboto -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<!-- GMP Bootstrap Loader -->
<script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "YOUR_API_KEY_HERE",
v: "weekly",
libraries: "places,maps,marker,geocoding"
});
</script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<!-- Header for search controls -->
<header class="top-header">
<div class="logo">
<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
<span>PlaceFinder</span>
</div>
<div class="search-container">
<input
type="text"
id="query-input"
placeholder="e.g., burger in New York"
value="burger"
/>
<button id="search-button" aria-label="Search">Search</button>
</div>
<div class="filter-container">
<label class="open-now-label">
<input type="checkbox" id="open-now-filter"> Open Now
</label>
<select id="rating-filter" aria-label="Minimum rating">
<option value="0" selected>Any rating</option>
<option value="1">1+ ★</option>
<option value="2">2+ ★★</option>
<option value="3">3+ ★★★</option>
<option value="4">4+ ★★★★</option>
<option value="5">5 ★★★★★</option>
</select>
<select id="price-filter" aria-label="Price level">
<option value="0" selected>Any Price</option>
<option value="1">$</option>
<option value="2">$$</option>
<option value="3">$$$</option>
<option value="4">$$$$</option>
</select>
</div>
</header>
<!-- Main content area -->
<div class="app-container">
<!-- Left Panel: Results -->
<div class="sidebar">
<div class="results-header">
<h2 id="results-header-text">Results</h2>
</div>
<div class="results-container">
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
<div id="placeholder-message" class="placeholder">
<p>Your search results will appear here.</p>
</div>
<div id="loading-spinner" class="spinner-overlay">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- Right Panel: Map -->
<div class="map-container">
<gmp-map
center="40.758896,-73.985130"
zoom="13"
map-id="DEMO_MAP_ID"
>
</gmp-map>
<div id="details-container">
<gmp-place-details-compact>
<gmp-place-details-place-request></gmp-place-details-place-request>
<gmp-place-all-content></gmp-place-all-content>
</gmp-place-details-compact>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Crea el archivo CSS
A continuación, crea un archivo llamado style.css
. Ahora agregaremos todos los estilos necesarios para establecer un aspecto limpio y moderno desde el principio. Este CSS controla el diseño general, los colores, las fuentes y la apariencia de todos los elementos de la IU.
Copia el siguiente código en style.css
:
/* style.css */
:root {
--primary-color: #1a73e8;
--text-color: #202124;
--text-color-light: #5f6368;
--background-color: #f8f9fa;
--panel-background: #ffffff;
--border-color: #dadce0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Roboto', sans-serif;
margin: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--background-color);
color: var(--text-color);
}
.hidden {
display: none !important;
}
.top-header {
display: flex;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid var(--border-color);
background-color: var(--panel-background);
gap: 24px;
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 22px;
font-weight: 700;
color: var(--primary-color);
}
.search-container {
display: flex;
flex-grow: 1;
max-width: 720px;
}
.search-container input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px 0 0 8px;
font-size: 16px;
transition: box-shadow 0.2s ease;
}
.search-container input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
.search-container button {
padding: 0 20px;
border: 1px solid var(--primary-color);
border-radius: 0 8px 8px 0;
background-color: var(--primary-color);
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.2s ease;
}
.search-container button:hover {
background-color: #185abc;
}
.filter-container {
display: flex;
gap: 12px;
align-items: center;
}
.filter-container select, .open-now-label {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--panel-background);
font-size: 14px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.filter-container select:hover, .open-now-label:hover {
border-color: #c0c2c5;
}
.open-now-label {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.app-container {
display: flex;
flex-grow: 1;
overflow: hidden;
}
.sidebar {
width: 35%;
min-width: 380px;
max-width: 480px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
background-color: var(--panel-background);
overflow: hidden;
}
.results-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.results-header h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.results-container {
flex-grow: 1;
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
.placeholder {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
box-sizing: border-box;
}
.placeholder p {
color: var(--text-color-light);
font-size: 1.1rem;
}
gmp-place-search {
width: 100%;
}
.map-container {
flex-grow: 1;
position: relative;
}
gmp-map {
width: 100%;
height: 100%;
}
.spinner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
opacity: 1;
visibility: visible;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e0e0e0;
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
gmp-place-details-compact {
width: 350px;
display: none;
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
gmp-place-details-compact::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 12px;
background-color: var(--panel-background);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
Crea la clase de aplicación de JavaScript
Por último, crea un archivo llamado script.js
. Estructuraremos nuestra aplicación dentro de una clase de JavaScript llamada PlaceFinderApp
. Esto mantiene nuestro código organizado y administra el estado de forma limpia.
Este código inicial definirá la clase, encontrará todos nuestros elementos HTML en el objeto constructor
y creará un método init()
para cargar las bibliotecas de Google Maps Platform.
Copia el siguiente código en script.js
:
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// We will add more initialization logic here in later steps.
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
Restricciones de la clave de API
Es posible que debas agregar una restricción nueva a tu clave de API para que este codelab funcione. Consulta Restringe tus claves de API para obtener más información y orientación sobre cómo hacerlo.
Revise su trabajo
Abre el archivo index.html
en tu navegador web. Deberías ver una página con un encabezado que contiene una barra de búsqueda y filtros, una barra lateral con el mensaje "Tus resultados de la búsqueda aparecerán aquí" y un mapa grande centrado en la ciudad de Nueva York. En esta etapa, los controles de búsqueda aún no funcionan.
4. Implementa una función de búsqueda
En esta sección, le daremos vida a nuestra aplicación implementando la funcionalidad de búsqueda principal. Escribiremos el código que se ejecuta cuando un usuario hace clic en el botón "Buscar". Compilaremos esta función con prácticas recomendadas desde el principio para controlar las interacciones del usuario de forma correcta y evitar errores comunes, como las condiciones de carrera.
Al final de este paso, podrás hacer clic en el botón de búsqueda y ver un indicador de carga mientras la aplicación recupera datos en segundo plano.
Crea el método de búsqueda
Primero, define el método performSearch
dentro de nuestra clase PlaceFinderApp
. Esta función será el corazón de nuestra lógica de búsqueda. También presentaremos una variable de instancia, isSearchInProgress
, para que actúe como un "portero". Esto evita que el usuario inicie una nueva búsqueda mientras ya hay una en curso, lo que puede generar errores.
La lógica dentro de performSearch
puede parecer compleja, por lo que la analizaremos:
- Primero, verifica si ya hay una búsqueda en curso. Si es así, no hace nada.
- Establece la marca
isSearchInProgress
entrue
para "bloquear" la función. - Muestra el spinner de carga y prepara la IU para los resultados nuevos.
- Establece la propiedad
textQuery
de la solicitud de búsqueda ennull
. Este es un paso crucial que obliga al componente web a reconocer que se está enviando una nueva solicitud. - Usa un
setTimeout
con una demora de0
. Esta técnica estándar de JavaScript programa el resto de nuestro código para que se ejecute en la siguiente tarea del navegador, lo que garantiza que el componente haya procesado primero el valor denull
. Incluso si el usuario busca exactamente lo mismo dos veces, siempre se activará una búsqueda nueva.
Agrega objetos de escucha de eventos
A continuación, debemos llamar a nuestro método performSearch
cuando el usuario interactúe con la app. Crearemos un método nuevo, attachEventListeners
, para mantener todo nuestro código de control de eventos en un solo lugar. Por ahora, solo agregaremos un objeto de escucha para el evento click
del botón de búsqueda. También agregaremos un marcador de posición para otro evento, gmp-load
, que usaremos en el siguiente paso.
Actualiza el archivo JavaScript
Actualiza tu archivo script.js
con el siguiente código. Las secciones nuevas o modificadas son el método attachEventListeners
y el método performSearch
.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// Call the new method to set up listeners
this.attachEventListeners();
}
// NEW: Method to set up all event listeners
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// We will add the gmp-load listener in the next step
}
// NEW: Core search method
async performSearch() {
// Exit if a search is already in progress
if (this.isSearchInProgress) {
return;
}
// Set the lock
this.isSearchInProgress = true;
// Show the placeholder and spinner
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// Force a state change by clearing the query first.
this.searchRequest.textQuery = null;
// Defer setting the real properties to the next event loop cycle.
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
// If the query is empty, release the lock and hide the spinner
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// For now, we just set the textQuery. We'll add filters later.
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
// NEW: Helper method to show/hide the spinner
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
Revise su trabajo
Guarda el archivo script.js
y actualiza index.html
en tu navegador. La página debería verse igual que antes. Ahora, haz clic en el botón “Buscar” del encabezado.
Deberías ver dos cosas:
- Desaparece el mensaje de marcador de posición "Los resultados de tu búsqueda aparecerán aquí".
- Aparece el ícono giratorio de carga y sigue girando.
El spinner girará indefinidamente porque aún no le indicamos cuándo detenerse. Lo haremos en la siguiente sección cuando mostremos los resultados. Esto confirma que nuestra función de búsqueda se activa correctamente.
5. Cómo mostrar los resultados y agregar marcadores
Ahora que el activador de búsqueda funciona, la siguiente tarea es mostrar los resultados en la pantalla. El código de esta sección conectará la lógica de búsqueda a la IU. Una vez que el elemento de Place Search termine de cargar los datos, liberará el "bloqueo" de la búsqueda, ocultará el indicador de carga y mostrará un marcador en el mapa para cada resultado.
Escucha la finalización de la búsqueda
El elemento Place Search activa un evento gmp-load
cuando recupera datos correctamente. Esta es la señal perfecta para procesar los resultados.
Primero, agrega un objeto de escucha de eventos para este evento en nuestro método attachEventListeners
.
Crea métodos de control de marcadores
A continuación, crearemos dos nuevos métodos de ayuda: clearMarkers
y addMarkers
.
clearMarkers()
quitará los marcadores de una búsqueda anterior.- Nuestro objeto de escucha
gmp-load
llamará aaddMarkers()
. Se ejecutará un bucle en la lista de lugares que devolvió la búsqueda y se creará un nuevoAdvancedMarkerElement
para cada uno. Aquí también ocultaremos el ícono giratorio de carga y liberaremos el bloqueo deisSearchInProgress
, lo que completará el ciclo de búsqueda.
Observa que almacenamos marcadores en un objeto (this.markers
) usando el ID de lugar como clave. Esta es una forma de administrar los marcadores y nos permitirá encontrar uno específico más adelante.
Por último, debemos llamar a clearMarkers()
al comienzo de cada búsqueda nueva. El mejor lugar para hacerlo es dentro de performSearch
.
Actualiza el archivo JavaScript
Actualiza tu archivo script.js
con los métodos nuevos y los cambios en attachEventListeners
y performSearch
.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// NEW: Listen for when the search component has loaded results
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
}
// NEW: Method to clear markers from a previous search
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// NEW: Method to add markers for new search results
addMarkers() {
// Release the lock and hide the spinner
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
// Create a new marker for each place result
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Store marker by its place ID for access later
this.markers[place.id] = marker;
}
}
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// NEW: Clear old markers before starting a new search
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
Revise su trabajo
Guarda los archivos y actualiza la página en tu navegador. Haz clic en el botón "Buscar".
El ícono giratorio de carga debería aparecer por un momento y, luego, desaparecer. En la barra lateral, se mostrará una lista de lugares relevantes para el término de búsqueda, y deberías ver los marcadores correspondientes en el mapa. Por el momento, los marcadores no hacen nada cuando se hace clic en ellos. Agregaremos esa interactividad en la próxima sección.
6. Activa los filtros de búsqueda y la interactividad de la lista
Nuestra aplicación ahora puede mostrar resultados de la búsqueda, pero aún no es interactiva. En esta sección, haremos que todos los controles del usuario cobren vida. Activaremos los filtros, habilitaremos la búsqueda con la tecla "Intro" y conectaremos los elementos de la lista de resultados con sus ubicaciones correspondientes en el mapa.
Al final de este paso, la aplicación se sentirá completamente responsiva a la entrada del usuario.
Activa los filtros de búsqueda
Primero, se actualizará el método performSearch
para leer los valores de todos los controles de filtro del encabezado. Para cada filtro (precio, calificación y "Abierto ahora"), se establecerá la propiedad correspondiente en el objeto searchRequest
antes de que se ejecute la búsqueda.
Agrega objetos de escucha de eventos para todos los controles
A continuación, expandiremos nuestro método attachEventListeners
. Agregaremos objetos de escucha para el evento change
en cada control de filtro, así como un objeto de escucha keydown
en la entrada de búsqueda para detectar cuando el usuario presione la tecla "Intro". Todos estos nuevos objetos de escucha llamarán al método performSearch
.
Conecta la lista de resultados al mapa
Para crear una experiencia fluida, cuando se hace clic en un elemento de la lista de resultados de la barra lateral, el mapa debe enfocarse en esa ubicación.
Un nuevo método, handleResultClick
, escuchará el evento gmp-select
, que se activa con el elemento Place Search cuando se hace clic en un elemento. Esta función encontrará la ubicación del lugar asociado y desplazará el mapa suavemente hacia ella.
Para que esto funcione, asegúrate de que el atributo selectable
esté presente en tu componente gmp-place-search
en index.html
.
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
Actualiza el archivo JavaScript
Actualiza el archivo script.js
con el siguiente código completo. Esta versión incluye el nuevo método handleResultClick
y la lógica actualizada en attachEventListeners
y performSearch
.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
// UPDATED: All event listeners are now attached
attachEventListeners() {
// Listen for the 'Enter' key press in the search input
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
// Listen for a sidebar result click
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
this.markers[place.id] = marker;
}
}
// NEW: Function to handle clicks on the results list
handleResultClick(event) {
const place = event.place;
if (!place || !place.location) return;
// Pan the map to the selected place
this.map.panTo(place.location);
}
// UPDATED: Search function now includes all filters
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
// Add filter values to the request
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
Revise su trabajo
Guarda el archivo script.js
y actualiza la página. Ahora la aplicación debería ser muy interactiva.
Verifica lo siguiente:
- La búsqueda funciona si presionas "Intro" en el cuadro de búsqueda.
- Si cambias alguno de los filtros (Precio, Calificación, Abierto ahora), se activará una nueva búsqueda y se actualizarán los resultados.
- Ahora, cuando haces clic en un elemento de la lista de resultados de la barra lateral, el mapa se desplaza suavemente hasta la ubicación de ese elemento.
En la siguiente sección, implementaremos la tarjeta de detalles que aparece cuando se hace clic en un marcador.
7. Implementa el elemento Place Details
Nuestra aplicación ahora es completamente interactiva, pero le falta una función clave: la capacidad de ver más información sobre un lugar seleccionado. En esta sección, implementaremos el elemento Place Details que aparecerá cuando un usuario haga clic en un marcador del mapa o seleccione un elemento en el elemento Place Search.
Crea un contenedor de tarjeta de detalles reutilizable
La forma más eficiente de mostrar los detalles de un lugar en el mapa es crear un solo contenedor reutilizable. Usaremos un AdvancedMarkerElement
como este contenedor. Su contenido será el widget gmp-place-details-compact
oculto que ya tenemos en nuestro index.html
.
Un nuevo método, initDetailsPopup
, controlará la creación de este marcador reutilizable. Se creará una vez cuando se cargue la aplicación y comenzará oculta. También agregaremos un objeto de escucha al mapa principal en este método, de modo que, si se hace clic en cualquier parte del mapa, se ocultará la tarjeta de detalles.
Actualiza el comportamiento de clic en el marcador
A continuación, debemos actualizar lo que sucede cuando un usuario hace clic en un marcador de lugar. El objeto de escucha 'click'
dentro del método addMarkers
ahora será responsable de mostrar la tarjeta de detalles.
Cuando se haga clic en un marcador, el objeto de escucha hará lo siguiente:
- Desplaza el mapa hasta la ubicación del marcador.
- Actualiza la tarjeta de detalles con la información de ese lugar específico.
- Coloca la tarjeta de detalles en la ubicación del marcador y hazla visible.
Conecta el clic en la lista con el clic en el marcador
Por último, actualizaremos el método handleResultClick
. En lugar de solo desplazar el mapa, ahora activará de forma programática el evento click
en el marcador correspondiente. Este es un patrón eficaz que nos permite reutilizar la misma lógica exacta para ambas interacciones, lo que mantiene nuestro código limpio y fácil de mantener.
Actualiza el archivo JavaScript
Actualiza tu archivo script.js
con el siguiente código. Las secciones nuevas o modificadas son el método initDetailsPopup
y los métodos addMarkers
y handleResultClick
actualizados.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// NEW: Call the method to initialize the details card
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
// NEW: Method to set up the reusable details card
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// UPDATED: The marker's click listener now shows the details card
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Add the click listener to show the details card
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
// UPDATED: This now triggers the marker's click event
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
// Programmatically trigger the marker's click event
marker.click();
}
}
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
// Hide the details card when a new search starts
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
Revise su trabajo
Guarda el archivo script.js
y actualiza la página. Ahora, la aplicación debería mostrar detalles a pedido.
Verifica lo siguiente:
- Al hacer clic en un marcador del mapa, ahora se centra el mapa y se abre una tarjeta de detalles con diseño sobre el marcador.
- Hacer clic en un elemento de la lista de resultados de la barra lateral tiene exactamente el mismo efecto.
- Si haces clic en el mapa fuera de la tarjeta, se cerrará.
- Si inicias una nueva búsqueda, también se cerrará cualquier tarjeta de detalles abierta.
8. Agrega los últimos detalles
Nuestra aplicación ahora es completamente funcional, pero podemos agregar algunos detalles finales para mejorar aún más la experiencia del usuario. En esta última sección, implementaremos dos funciones clave: un encabezado dinámico que proporciona un mejor contexto para los resultados de la búsqueda y un formato automático para la búsqueda del usuario.
Crea un encabezado de resultados dinámico
Por el momento, el encabezado de la barra lateral siempre dice "Resultados". Podemos hacer que sea más informativa si la actualizamos para que refleje la búsqueda actual. Por ejemplo, "Hamburguesas cerca de Nueva York".
Para ello, usaremos la API de Geocoding para convertir las coordenadas centrales del mapa en una ubicación legible por humanos, como el nombre de una ciudad. Un nuevo método async
, updateResultsHeader
, controlará esta lógica. Se llamará cada vez que se realice una búsqueda.
Da formato a la búsqueda del usuario
Para asegurarnos de que la IU se vea limpia y coherente, formatearemos automáticamente el término de búsqueda del usuario en "Formato de título" (p.ej., "burger restaurant" se convierte en "Burger Restaurant"). Una función auxiliar, toTitleCase
, controlará esta transformación. El método performSearch
se actualizará para usar esta función en la entrada del usuario antes de realizar la búsqueda y actualizar el encabezado.
Actualiza el archivo JavaScript
Actualiza tu archivo script.js
con la versión final del código. Esto incluye los nuevos métodos toTitleCase
y updateResultsHeader
, y el método performSearch
actualizado que los integra.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
// NEW: Helper function to format text to Title Case
toTitleCase(str) {
if (!str) return '';
return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
clearMarkers() {
for (const marker of Object.values(this.markers)) { marker.map = null; }
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
marker.click();
}
}
// UPDATED: Now integrates formatting and the dynamic header
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// Format the query and update the input box value
const formattedQuery = this.toTitleCase(rawQuery);
this.queryInput.value = formattedQuery;
// Update the header with the new query and location
await this.updateResultsHeader(formattedQuery);
// Pass the formatted query to the search request
this.searchRequest.textQuery = formattedQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
// NEW: Method to update the sidebar header with geocoded location
async updateResultsHeader(query) {
try {
const response = await this.geocoder.geocode({ location: this.map.getCenter() });
if (response.results && response.results.length > 0) {
const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
const city = cityResult.address_components[0].long_name;
this.resultsHeaderText.textContent = `${query} near ${city}`;
} else {
this.resultsHeaderText.textContent = `${query} near current map area`;
}
} catch (error) {
console.error("Geocoding failed:", error);
this.resultsHeaderText.textContent = `Results for ${query}`;
}
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
Revise su trabajo
Guarda el archivo script.js
y actualiza la página.
Verifica las funciones:
- Escribe
pizza
(todo en minúsculas) en el cuadro de búsqueda y haz clic en Buscar. El texto del cuadro debería cambiar a "Pizza", y el encabezado de la barra lateral debería actualizarse para indicar "Pizza cerca de Nueva York". - Desplaza el mapa a otra ciudad, como Boston, y vuelve a buscar. El encabezado debería actualizarse a "Pizzerías cerca de Boston".
9. Felicitaciones
Creaste correctamente una aplicación de búsqueda local interactiva y completa que combina la simplicidad del kit de IU de Places con la potencia de las APIs principales de JavaScript de Google Maps Platform.
Qué aprendiste
- Cómo estructurar una aplicación de mapas con una clase de JavaScript para administrar el estado y la lógica
- Cómo usar el kit de IU de Places con la API de Google Maps JavaScript para desarrollar rápidamente la IU
- Cómo agregar y administrar marcadores avanzados de forma programática para mostrar lugares de interés personalizados en el mapa
- Cómo usar el servicio de Geocodificación para convertir coordenadas en direcciones legibles por humanos y mejorar la experiencia del usuario
- Cómo identificar y corregir condiciones de carrera comunes en una aplicación interactiva usando marcas de estado y asegurándose de que las propiedades de los componentes se actualicen correctamente
Próximos pasos
- Obtén más información para personalizar los Marcadores avanzados cambiando su color, escala o incluso usando HTML personalizado.
- Explora el diseño de mapas basado en Cloud para personalizar el aspecto de tu mapa y que coincida con tu marca.
- Intenta agregar la Biblioteca de dibujo para permitir que los usuarios dibujen formas en el mapa y definan áreas de búsqueda.
- Responde la siguiente encuesta para ayudarnos a crear el contenido que te resultaría más útil:
¿Qué otros codelabs te gustaría ver?
¿No encuentras el codelab que más te interesa? Crea un nuevo problema aquí para solicitarlo.