Integração das Google Maps APIs no Federal Pro
Guia completo de arquitetura, serviços e integração das APIs de Directions, Geocoding, Places e Distance Matrix — com coloração dinâmica de rotas por trânsito em tempo real.
Visão Geral
O objetivo é exibir um mapa interativo com rotas coloridas por condição de trânsito, usando as APIs REST do Google Maps Platform. Cada segmento da rota recebe uma cor (verde, amarelo ou vermelho) conforme o rácio entre o tempo estimado e o tempo real com tráfego.
Calcula rotas com dados de trânsito em tempo real.
Converte coordenadas em endereços e vice-versa.
Autocomplete de endereços na tela de busca.
Regra de ouro: componentes React nunca chamam a Google diretamente. Toda a comunicação passa por services/, que é consumido por hooks/, que por sua vez alimenta os componentes.
Arquitetura de Pastas
Configuração da Chave
A chave da API nunca deve estar escrita no código. Use variáveis de ambiente no app.config.js.
import 'dotenv/config'; export default { expo: { extra: { googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY, }, }, };
GOOGLE_MAPS_API_KEY=AIzaSy...
Atenção: no mobile, a chave fica sempre embutida no binário. A única forma de a proteger de verdade é passar as chamadas por um proxy backend. Para desenvolvimento, restrições por package name / bundle ID são suficientes.
Serviço Base — client.ts
import Constants from 'expo-constants'; const API_KEY = Constants.expoConfig?.extra?.googleMapsApiKey ?? process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY ?? ''; if (!API_KEY) { console.warn('[GoogleMaps] Chave de API não configurada!'); } const BASE_URL = 'https://maps.googleapis.com/maps/api'; /** * Wrapper genérico para chamadas às APIs REST do Google. * Centraliza erros, timeout e a chave. */ export async function googleFetch<T>( endpoint: string, params: Record<string, string | number>, timeoutMs = 8000 ): Promise<T> { const url = new URL(`${BASE_URL}${endpoint}`); Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)) ); url.searchParams.set('key', API_KEY); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url.toString(), { signal: controller.signal }); const data = await res.json(); if (data?.status && data.status !== 'OK' && data.status !== 'ZERO_RESULTS') { throw new Error( `[GoogleMaps] ${endpoint} → ${data.status}: ${data.error_message ?? ''}` ); } return data as T; } finally { clearTimeout(timer); } }
Directions API — directions.ts
Esta é a API central da lógica de coloração. O parâmetro departure_time=now é obrigatório para obter duration_in_traffic.
import polyline from '@mapbox/polyline'; import { googleFetch } from './client'; export type LatLng = { latitude: number; longitude: number }; export type RouteSegment = { coordinates: LatLng[]; color: string; duration: number; durationInTraffic: number; }; function colorFor(duration: number, durationInTraffic: number): string { const ratio = duration > 0 ? durationInTraffic / duration : 1; if (ratio < 1.1) return '#16A34A'; // verde if (ratio < 1.5) return '#F59E0B'; // amarelo return '#DC2626'; // vermelho } export async function getRoute( origin: LatLng, destination: LatLng, waypoints: LatLng[] = [] ) { const wp = waypoints .map((p) => `${p.latitude},${p.longitude}`) .join('|'); const data = await googleFetch<any>('/directions/json', { origin: `${origin.latitude},${origin.longitude}`, destination: `${destination.latitude},${destination.longitude}`, ...(wp ? { waypoints: wp } : {}), departure_time: 'now', traffic_model: 'best_guess', mode: 'driving', language: 'pt-BR', units: 'metric', }); if (!data.routes?.length) return null; const route = data.routes[0]; const leg = route.legs[0]; const decoded = polyline.decode(route.overview_polyline.points); const coords = decoded.map(([lat, lng]) => ({ latitude: lat, longitude: lng, })); const color = colorFor( leg.duration?.value ?? 0, leg.duration_in_traffic?.value ?? leg.duration?.value ?? 0 ); return { segments: [{ coordinates: coords, color, duration: leg.duration.value, durationInTraffic: leg.duration_in_traffic?.value ?? leg.duration.value, }], distanceMeters: leg.distance.value, durationSeconds: leg.duration.value, durationInTrafficSeconds: leg.duration_in_traffic?.value ?? leg.duration.value, polyline: coords, }; }
Nota: a segmentação simplificada colore a rota inteira. Para colorir por "step" (mais preciso), percorra leg.steps e construa um segmento por step.
Geocoding API — geocoding.ts
import { googleFetch } from './client'; import type { LatLng } from './directions'; export async function reverseGeocode(coord: LatLng) { const data = await googleFetch<any>('/geocode/json', { latlng: `${coord.latitude},${coord.longitude}`, language: 'pt-BR', }); if (!data.results?.length) return null; return data.results[0].formatted_address ?? null; } export async function geocodeAddress(address: string) { const data = await googleFetch<any>('/geocode/json', { address, language: 'pt-BR', }); if (!data.results?.length) return null; const loc = data.results[0].geometry.location; return { latitude: loc.lat, longitude: loc.lng }; }
Places API — places.ts
import { googleFetch } from './client'; import type { LatLng } from './directions'; export type PlaceSuggestion = { placeId: string; primary: string; secondary: string; }; export async function autocompleteAddress(input: string, near?: LatLng) { if (input.trim().length < 3) return []; const data = await googleFetch<any>('/place/autocomplete/json', { input, language: 'pt-BR', components: 'country:br', ...(near ? { location: `${near.latitude},${near.longitude}`, radius: 50000, } : {}), }); return (data.predictions ?? []).map((p: any) => ({ placeId: p.place_id, primary: p.structured_formatting?.main_text ?? p.description, secondary: p.structured_formatting?.secondary_text ?? '', })); } export async function getPlaceDetails(placeId: string) { const data = await googleFetch<any>('/place/details/json', { place_id: placeId, fields: 'formatted_address,geometry', language: 'pt-BR', }); if (!data.result) return null; return { address: data.result.formatted_address, coord: { latitude: data.result.geometry.location.lat, longitude: data.result.geometry.location.lng, }, }; }
Distance Matrix — distanceMatrix.ts
import { googleFetch } from './client'; import type { LatLng } from './directions'; export async function getEtaMatrix( origins: LatLng[], destinations: LatLng[] ) { const data = await googleFetch<any>('/distancematrix/json', { origins: origins.map((p) => `${p.latitude},${p.longitude}`).join('|'), destinations: destinations.map((p) => `${p.latitude},${p.longitude}`).join('|'), departure_time: 'now', traffic_model: 'best_guess', language: 'pt-BR', units: 'metric', }); return (data.rows ?? []).flatMap((row: any) => (row.elements ?? []).map((el: any) => ({ distanceMeters: el.distance?.value ?? 0, durationSeconds: el.duration?.value ?? 0, durationInTrafficSeconds: el.duration_in_traffic?.value ?? el.duration?.value ?? 0, })) ); }
Hooks de Consumo
import { useEffect, useState } from 'react'; import { getRoute, type LatLng, type RouteResult } from '@/services/googleMaps/directions'; export function useRoute(origin: LatLng | null, destination: LatLng | null) { const [route, setRoute] = useState<RouteResult | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); useEffect(() => { if (!origin || !destination) { setRoute(null); return; } let cancelled = false; setLoading(true); setError(null); getRoute(origin, destination) .then((r) => { if (!cancelled) setRoute(r); }) .catch((e) => { if (!cancelled) setError(e?.message ?? 'Erro ao calcular rota'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [origin?.latitude, origin?.longitude, destination?.latitude, destination?.longitude]); return { route, loading, error }; }
Componente Mapa
import MapView, { PROVIDER_GOOGLE, Polyline, Marker } from 'react-native-maps'; import { useRoute } from '@/hooks/useRoute'; import type { LatLng } from '@/services/googleMaps/directions'; export function GoogleMap({ origin, destination, }: { origin: LatLng; destination: LatLng; }) { const { route } = useRoute(origin, destination); return ( <MapView style={{ flex: 1 }} provider={PROVIDER_GOOGLE} initialRegion={{ latitude: origin.latitude, longitude: origin.longitude, latitudeDelta: 0.05, longitudeDelta: 0.05, }} > {route?.segments.map((seg, i) => ( <Polyline key={i} coordinates={seg.coordinates} strokeColor={seg.color} strokeWidth={6} /> ))} <Marker coordinate={origin} title="Origem" /> <Marker coordinate={destination} title="Destino" /> </MapView> ); }
Integração no index.tsx
import { GoogleMap } from '@/components/GoogleMap'; import { reverseGeocode } from '@/services/googleMaps/geocoding'; // Dentro do componente Home: const destino = { latitude: -23.56, longitude: -46.65 }; // Reverse geocoding do endereço atual: useEffect(() => { if (!coords) return; reverseGeocode(coords).then((addr) => addr && setAddress(addr)); }, [coords?.latitude, coords?.longitude]); // No render: <GoogleMap origin={coords} destination={destino} />
Proxy Backend (Produção)
Quando o projeto crescer, mova as chamadas para o seu backend PHP. Em vez de googleFetch chamar diretamente a Google, chama o seu servidor.
const BASE_URL = 'https://federalpro.com.br/backend/api/google'; export async function googleFetch<T>(endpoint: string, params: Record<string, any>) { const res = await fetch(`${BASE_URL}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify(params), }); return res.json(); }
- Chave nunca chega ao cliente
- Cache e rate limiting no servidor
- Logging centralizado
- Agrupar várias chamadas numa só
- Latência extra (hop no servidor)
- Necessário gerir tokens de auth
- Endpoints PHP a manter
- Cuidado com quotas agregadas
Resumo Geral
| Ficheiro | Responsabilidade | Consumido por | Tipo |
|---|---|---|---|
| client.ts | Chave, URL base, timeout, erros | Os outros serviços | Base |
| directions.ts | Rotas + trânsito + cores | useRoute | Gratuito |
| geocoding.ts | Coordenadas ↔ endereço | index.tsx | Gratuito |
| places.ts | Autocomplete + detalhes | useAddressSearch | Gratuito |
| distanceMatrix.ts | ETA / distância em lote | Lógica de tarifas | Gratuito |
| useRoute.ts | Estado (loading, data, error) | GoogleMap.tsx | Hook |
| GoogleMap.tsx | Renderiza MapView + Polyline | index.tsx | Componente |
Regra final: componentes não sabem o que é a Google. Serviços não sabem o que é React. Hooks ligam os dois. É exatamente o padrão que já usas com locationApi.ts e useLocation.
Cores de Trânsito
Rácio duration_in_traffic / duration |
Cor | Significado |
|---|---|---|
| < 1.1 | #16A34A | Trânsito normal (verde) |
| 1.1 – 1.5 | #F59E0B | Trânsito moderado (amarelo) |
| > 1.5 | #DC2626 | Congestionado (vermelho) |
Dica: para colorir por step em vez da rota inteira, percorra leg.steps, e para cada step descodifique step.polyline.points e aplique colorFor(step.duration.value, step.duration_in_traffic?.value).