1. Antes de começar
Este codelab ensina a criar um aplicativo de pesquisa local totalmente interativo usando o Kit de Interface do Places da Plataforma Google Maps.
Pré-requisitos
- Um projeto do Google Cloud com as APIs e credenciais necessárias configuradas.
- Ter conhecimento básico de HTML e CSS.
- Conhecimento de JavaScript moderno.
- Um navegador da Web moderno, como a versão mais recente do Chrome.
- Um editor de texto de sua preferência.
Atividades deste laboratório
- Estruturar um aplicativo de mapeamento usando uma classe JavaScript.
- Usar componentes da Web para mostrar um mapa
- Use o elemento de pesquisa de lugar para realizar e mostrar os resultados de uma pesquisa de texto.
- Crie e gerencie marcadores de mapa
AdvancedMarkerElement
personalizados de maneira programática. - Mostrar o elemento de detalhes do lugar quando um usuário selecionar um local.
- Use a API Geocoding para criar uma interface dinâmica e fácil de usar.
O que é necessário
- Tenha um projeto do Google Cloud com o faturamento ativado.
- Uma chave de API da Plataforma Google Maps
- Um ID do mapa
- As seguintes APIs ativadas:
- API Maps JavaScript
- Kit de interface do Google Places
- API Geocoding
2. Começar a configuração
Para a etapa de ativação a seguir, é necessário ativar a API Maps JavaScript, o kit de interface do usuário Places e a API Geocoding.
Configurar a Plataforma Google Maps
Caso você ainda não tenha uma conta do Google Cloud Platform e um projeto com faturamento ativado, veja como criá-los no guia da Plataforma Google Maps.
- No Console do Cloud, clique no menu suspenso do projeto e selecione o projeto que você quer usar neste codelab.
- Ative as APIs e os SDKs da Plataforma Google Maps necessários para este codelab no Google Cloud Marketplace. Para fazer isso, siga as etapas descritas neste vídeo ou nesta documentação.
- Gere uma chave de API na página Credenciais do Console do Cloud. Siga as etapas indicadas neste vídeo ou nesta documentação. Todas as solicitações à Plataforma Google Maps exigem uma chave de API.
3. O shell do aplicativo e um mapa funcional
Nesta primeira etapa, vamos criar o layout visual completo do aplicativo e estabelecer uma estrutura limpa baseada em classes para o JavaScript. Isso nos dá uma base sólida para construir. Ao final desta seção, você terá uma página estilizada mostrando um mapa interativo.
Criar o arquivo HTML
Primeiro, crie um arquivo chamado index.html
. Esse arquivo vai conter a estrutura completa do nosso aplicativo, incluindo o cabeçalho, filtros de pesquisa, barra lateral, contêiner do mapa e os componentes da Web necessários.
Copie o seguinte código em index.html
. Substitua YOUR_API_KEY_HERE
pela sua chave de API da Plataforma Google Maps e DEMO_MAP_ID
pelo ID do mapa da Plataforma Google Maps.
<!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>
Criar o arquivo CSS
Em seguida, crie um arquivo chamado style.css
. Vamos adicionar todo o estilo necessário agora para estabelecer uma aparência limpa e moderna desde o início. Esse CSS processa o layout geral, as cores, as fontes e a aparência de todos os elementos da interface.
Copie o seguinte código em 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);
}
Criar a classe de aplicativo JavaScript
Por fim, crie um arquivo chamado script.js
. Vamos estruturar nosso aplicativo em uma classe JavaScript chamada PlaceFinderApp
. Isso mantém nosso código organizado e gerencia o estado de maneira limpa.
Esse código inicial vai definir a classe, encontrar todos os elementos HTML no constructor
e criar um método init()
para carregar as bibliotecas da Plataforma Google Maps.
Copie o seguinte código em 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();
});
Restrições da chave de API
Talvez seja necessário adicionar uma nova restrição à sua chave de API para que este codelab funcione. Consulte Restringir suas chaves de API para mais informações e orientações sobre como fazer isso.
Confira suas tarefas
Abra o arquivo index.html
no navegador da Web. Você vai encontrar uma página com um cabeçalho que contém uma barra de pesquisa e filtros, uma barra lateral com a mensagem "Seus resultados da pesquisa vão aparecer aqui" e um mapa grande centralizado na cidade de Nova York. Nessa etapa, os controles de pesquisa ainda não estão funcionando.
4. Implementar uma função de pesquisa
Nesta seção, vamos dar vida ao nosso aplicativo implementando a funcionalidade principal de pesquisa. Vamos escrever o código que é executado quando um usuário clica no botão "Pesquisar". Vamos criar essa função com práticas recomendadas desde o início para processar as interações do usuário de maneira adequada e evitar bugs comuns, como condições de corrida.
Ao final desta etapa, você poderá clicar no botão de pesquisa e ver um spinner de carregamento aparecer enquanto o aplicativo busca dados em segundo plano.
Criar o método de pesquisa
Primeiro, defina o método performSearch
dentro da classe PlaceFinderApp
. Essa função será a essência da nossa lógica de pesquisa. Também vamos apresentar uma variável de instância, isSearchInProgress
, para atuar como um "gatekeeper". Isso impede que o usuário inicie uma nova pesquisa enquanto uma já está em andamento, o que pode causar erros.
A lógica dentro de performSearch
pode parecer complexa, então vamos explicar:
- Primeiro, ela verifica se uma pesquisa já está em andamento. Se for, nada acontece.
- Ele define a flag
isSearchInProgress
comotrue
para "bloquear" a função. - Ele mostra o spinner de carregamento e prepara a interface para novos resultados.
- Ele define a propriedade
textQuery
da solicitação de pesquisa comonull
. Essa é uma etapa crucial que força o componente da Web a reconhecer que uma nova solicitação está chegando. - Ele usa um
setTimeout
com um atraso de0
. Essa técnica padrão do JavaScript agenda o restante do nosso código para ser executado na próxima tarefa do navegador, garantindo que o componente tenha processado o valornull
primeiro. Mesmo que o usuário pesquise exatamente a mesma coisa duas vezes, uma nova pesquisa sempre será acionada.
Adicione listeners de eventos
Em seguida, precisamos chamar o método performSearch
quando o usuário interage com o app. Vamos criar um novo método, attachEventListeners
, para manter todo o código de processamento de eventos em um só lugar. Por enquanto, vamos adicionar um listener ao evento click
do botão de pesquisa. Também vamos adicionar um marcador de posição para outro evento, gmp-load
, que usaremos na próxima etapa.
Atualizar o arquivo JavaScript
Atualize o arquivo script.js
com o seguinte código. As seções novas ou alteradas são os métodos attachEventListeners
e 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();
});
Confira suas tarefas
Salve o arquivo script.js
e atualize index.html
no navegador. A página vai parecer a mesma de antes. Agora, clique no botão "Pesquisar" no cabeçalho.
Duas coisas vão acontecer:
- A mensagem de marcador de posição "Os resultados da pesquisa vão aparecer aqui" desaparece.
- O ícone de carregamento aparece e continua girando.
O spinner vai girar para sempre porque ainda não dissemos quando ele deve parar. Vamos fazer isso na próxima seção, quando mostrarmos os resultados. Isso confirma que nossa função de pesquisa está sendo acionada corretamente.
5. Mostrar resultados e adicionar marcadores
Agora que o gatilho de pesquisa está funcionando, a próxima tarefa é mostrar os resultados na tela. O código nesta seção vai conectar a lógica de pesquisa à interface. Quando o elemento de pesquisa de lugar terminar de carregar os dados, ele vai liberar o "bloqueio" da pesquisa, ocultar o ícone de carregamento e mostrar um marcador no mapa para cada resultado.
Ouvir a conclusão da pesquisa
O elemento de pesquisa de lugar aciona um evento gmp-load
quando busca dados com sucesso. Esse é o indicador perfeito para processarmos os resultados.
Primeiro, adicione um listener para esse evento no método attachEventListeners
.
Criar métodos de processamento de marcadores
Em seguida, vamos criar dois novos métodos auxiliares: clearMarkers
e addMarkers
.
clearMarkers()
remove todos os marcadores de uma pesquisa anterior.- O
addMarkers()
será chamado pelo nosso listenergmp-load
. Ele vai percorrer a lista de lugares retornados pela pesquisa e criar um novoAdvancedMarkerElement
para cada um. É aqui também que vamos ocultar o ícone de carregamento e liberar o bloqueioisSearchInProgress
, concluindo o ciclo de pesquisa.
Observe que estamos armazenando marcadores em um objeto (this.markers
) usando o ID do lugar como chave. Essa é uma maneira de gerenciar marcadores e nos permite encontrar um marcador específico mais tarde.
Por fim, precisamos chamar clearMarkers()
no início de cada nova pesquisa. O melhor lugar para isso é dentro de performSearch
.
Atualizar o arquivo JavaScript
Atualize o arquivo script.js
com os novos métodos e as mudanças em attachEventListeners
e 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();
});
Confira suas tarefas
Salve os arquivos e atualize a página no navegador. Clique no botão "Pesquisar".
O ícone de carregamento vai aparecer por um momento e depois desaparecer. A barra lateral será preenchida com uma lista de lugares relevantes para o termo de pesquisa, e os marcadores correspondentes vão aparecer no mapa. Os marcadores ainda não fazem nada quando clicados. Vamos adicionar essa interatividade na próxima seção.
6. Ativar os filtros de pesquisa e a interatividade da lista
Nosso aplicativo agora pode mostrar resultados da pesquisa, mas ainda não é interativo. Nesta seção, vamos dar vida a todos os controles do usuário. Vamos ativar os filtros, permitir a pesquisa com a tecla "Enter" e conectar os itens na lista de resultados aos locais correspondentes no mapa.
Ao final desta etapa, o aplicativo vai parecer totalmente responsivo à entrada do usuário.
Ativar os filtros de pesquisa
Primeiro, o método performSearch
será atualizado para ler os valores de todos os controles de filtro no cabeçalho. Para cada filtro (preço, classificação e "Aberto agora"), a propriedade correspondente será definida no objeto searchRequest
antes da execução da pesquisa.
Adicionar listeners de eventos para todos os controles
Em seguida, vamos expandir nosso método attachEventListeners
. Vamos adicionar listeners para o evento change
em cada controle de filtro, bem como um listener keydown
na entrada de pesquisa para detectar quando o usuário pressiona a tecla "Enter". Todos esses novos listeners vão chamar o método performSearch
.
Conectar a lista de resultados ao mapa
Para criar uma experiência perfeita, clicar em um item na lista de resultados da barra lateral deve focar o mapa nesse local.
Um novo método, handleResultClick
, vai detectar o evento gmp-select
, que é disparado pelo elemento de pesquisa de lugar quando um item é clicado. Essa função encontra o local associado e move o mapa até ele.
Para que isso funcione, verifique se o atributo selectable
está presente no componente gmp-place-search
em 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>
Atualizar o arquivo JavaScript
Atualize o arquivo script.js
com o código completo a seguir. Essa versão inclui o novo método handleResultClick
e a lógica atualizada em attachEventListeners
e 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();
});
Confira suas tarefas
Salve o arquivo script.js
e atualize a página. O aplicativo agora deve ser altamente interativo.
Confirme o seguinte:
- A pesquisa pressionando "Enter" na caixa de pesquisa funciona.
- Mudar qualquer um dos filtros (Preço, Avaliação, Aberto agora) aciona uma nova pesquisa e atualiza os resultados.
- Ao clicar em um item na lista de resultados da barra lateral, o mapa agora se move suavemente para a localização desse item.
Na próxima seção, vamos implementar o card de detalhes que aparece quando um marcador é clicado.
7. Implementar o elemento Place Details
Nosso aplicativo agora é totalmente interativo, mas está faltando um recurso importante: a capacidade de ver mais informações sobre um lugar selecionado. Nesta seção, vamos implementar o elemento de detalhes do lugar que aparece quando um usuário clica em um marcador no mapa ou seleciona um item no elemento de pesquisa de lugar.
Criar um contêiner de card de detalhes reutilizável
A maneira mais eficiente de mostrar detalhes de lugares no mapa é criar um único contêiner reutilizável. Vamos usar um AdvancedMarkerElement
como contêiner. O conteúdo dele será o widget gmp-place-details-compact
oculto que já temos no nosso index.html
.
Um novo método, initDetailsPopup
, vai processar a criação desse marcador reutilizável. Ele será criado uma vez quando o aplicativo for carregado e vai começar oculto. Também vamos adicionar um listener ao mapa principal nesse método para que clicar em qualquer lugar no mapa oculte o card de detalhes.
Atualizar o comportamento de clique no marcador
Em seguida, precisamos atualizar o que acontece quando um usuário clica em um marcador de lugar. O listener 'click'
dentro do método addMarkers
agora será responsável por mostrar o cartão de detalhes.
Quando um marcador é clicado, o listener faz o seguinte:
- Mova o mapa até a localização do marcador.
- Atualize o card de detalhes com as informações desse lugar específico.
- Posicione o card de detalhes no local do marcador e deixe-o visível.
Conectar o clique na lista ao clique no marcador
Por fim, vamos atualizar o método handleResultClick
. Em vez de apenas mover o mapa, ele agora vai acionar de maneira programática o evento click
no marcador correspondente. Esse é um padrão eficiente que permite reutilizar a mesma lógica para as duas interações, mantendo o código limpo e fácil de manter.
Atualizar o arquivo JavaScript
Atualize o arquivo script.js
com o seguinte código. As seções novas ou alteradas são o método initDetailsPopup
e os métodos addMarkers
e handleResultClick
atualizados.
// 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();
});
Confira suas tarefas
Salve o arquivo script.js
e atualize a página. O aplicativo vai mostrar detalhes sob demanda.
Confirme o seguinte:
- Ao clicar em um marcador no mapa, ele é centralizado e um card de detalhes estilizado é aberto sobre o marcador.
- Clicar em um item na lista de resultados da barra lateral faz exatamente a mesma coisa.
- Clicar no mapa longe do card fecha ele.
- Iniciar uma nova pesquisa também fecha qualquer card de detalhes aberto.
8. Dê o toque final
Nosso aplicativo agora está totalmente funcional, mas podemos adicionar alguns toques finais para melhorar ainda mais a experiência do usuário. Nesta última seção, vamos implementar dois recursos principais: um cabeçalho dinâmico que oferece um contexto melhor para os resultados da pesquisa e formatação automática para a consulta de pesquisa do usuário.
Criar um cabeçalho de resultados dinâmico
No momento, o cabeçalho da barra lateral sempre diz "Resultados". Podemos tornar isso mais informativo atualizando para refletir a pesquisa atual. Por exemplo, "Hambúrgueres perto de São Paulo".
Para isso, vamos usar a API Geocoding para converter as coordenadas centrais do mapa em um local legível, como o nome de uma cidade. Um novo método async
, updateResultsHeader
, vai processar essa lógica. Ele será chamado sempre que uma pesquisa for realizada.
Formatar a consulta de pesquisa do usuário
Para garantir que a interface pareça limpa e consistente, vamos formatar automaticamente o termo de pesquisa do usuário em "Title Case" (por exemplo, "restaurante de hambúrguer" se torna "Restaurante de hambúrguer". Uma função auxiliar, toTitleCase
, vai processar essa transformação. O método performSearch
será atualizado para usar essa função na entrada do usuário antes de realizar a pesquisa e atualizar o cabeçalho.
Atualizar o arquivo JavaScript
Atualize o arquivo script.js
com a versão final do código. Isso inclui os novos métodos toTitleCase
e updateResultsHeader
e o método performSearch
atualizado que os 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();
});
Confira suas tarefas
Salve o arquivo script.js
e atualize a página.
Verifique os recursos:
- Digite
pizza
(tudo em minúsculas) na caixa de pesquisa e clique em "Pesquisar". O texto na caixa vai mudar para "Pizza", e o cabeçalho na barra lateral vai ser atualizado para "Pizza perto de Nova York". - Mova o mapa para outra cidade, como Boston, e pesquise novamente. O cabeçalho vai mudar para "Pizza perto de Boston".
9. Parabéns
Você criou um aplicativo de pesquisa local completo e interativo que combina a simplicidade do Kit de Interface do Places com o poder das principais APIs JavaScript da Plataforma Google Maps.
O que você aprendeu
- Como estruturar um aplicativo de mapeamento usando uma classe JavaScript para gerenciar estado e lógica.
- Como usar o Kit de Interface do Places com a API Google Maps JavaScript para desenvolvimento rápido de interface.
- Como adicionar e gerenciar de forma programática Marcadores avançados para mostrar pontos de interesse personalizados no mapa.
- Como usar o serviço de geocodificação para transformar coordenadas em endereços legíveis e melhorar a experiência do usuário.
- Como identificar e corrigir condições de disputa comuns em um aplicativo interativo usando flags de estado e garantindo que as propriedades do componente sejam atualizadas corretamente.
A seguir
- Saiba mais sobre como personalizar Marcadores Avançados mudando a cor, a escala ou até mesmo usando HTML personalizado.
- Confira a Estilização de mapas baseada na nuvem para personalizar a aparência do mapa e combinar com sua marca.
- Adicione a Biblioteca de desenhos para permitir que os usuários desenhem formas no mapa e definam áreas de pesquisa.
- Ajude-nos a criar o conteúdo mais útil para você respondendo à seguinte pesquisa:
Quais outros codelabs você quer ver?
Não encontrou o codelab que mais interessa? Solicite-o aqui.