Documentação Técnica

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.

01

Visão Geral

O que vamos construir

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.

🧭 Directions API

Calcula rotas com dados de trânsito em tempo real.

📍 Geocoding API

Converte coordenadas em endereços e vice-versa.

🔎 Places API

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.

02

Arquitetura de Pastas

Organização dos ficheiros
src/ ├── services/ │ ├── googleMaps/ │ │ ├── client.ts ← configuração base, fetch, timeout │ │ ├── directions.ts ← rotas + trânsito + cores │ │ ├── geocoding.ts ← coordenadas ↔ endereço │ │ ├── places.ts ← autocomplete de endereços │ │ └── distanceMatrix.ts ← ETA / distância em lote │ └── locationApi.ts ← o seu serviço existente ├── hooks/ │ ├── useRoute.ts ← consome directions.ts │ └── useAddressSearch.ts ← consome places.ts ├── components/ │ └── GoogleMap.tsx ← renderiza MapView + Polyline └── app/(app)/ └── index.tsx ← usa os hooks
Componente
→
Hook
→
Service
→
Google API
03

Configuração da Chave

Variáveis de ambiente

A chave da API nunca deve estar escrita no código. Use variáveis de ambiente no app.config.js.

app.config.js
import 'dotenv/config';

export default {
  expo: {
    extra: {
      googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
    },
  },
};
.env
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.

04

Serviço Base — client.ts

Centraliza URL, chave, timeout e tratamento de erros
src/services/googleMaps/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);
  }
}
05

Directions API — directions.ts

Rotas + trânsito em tempo real + cores

Esta é a API central da lógica de coloração. O parâmetro departure_time=now é obrigatório para obter duration_in_traffic.

src/services/googleMaps/directions.ts
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.

06

Geocoding API — geocoding.ts

Coordenadas ↔ endereço
src/services/googleMaps/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 };
}
07

Places API — places.ts

Autocomplete de endereços
src/services/googleMaps/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,
    },
  };
}
08

Distance Matrix — distanceMatrix.ts

ETA e distância em lote
src/services/googleMaps/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,
    }))
  );
}
09

Hooks de Consumo

O fio condutor entre serviço e componente
src/hooks/useRoute.ts
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 };
}
10

Componente Mapa

Renderiza o mapa e as polylines coloridas
src/components/GoogleMap.tsx
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>
  );
}
11

Integração no index.tsx

Como tudo se liga ao componente principal
src/app/(app)/index.tsx — excerto
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} />
12

Proxy Backend (Produção)

Proteger a chave e centralizar chamadas

Quando o projeto crescer, mova as chamadas para o seu backend PHP. Em vez de googleFetch chamar diretamente a Google, chama o seu servidor.

src/services/googleMaps/client.ts (proxy mode)
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();
}
🔐 Vantagens
  • Chave nunca chega ao cliente
  • Cache e rate limiting no servidor
  • Logging centralizado
  • Agrupar várias chamadas numa só
⚠️ Custos
  • Latência extra (hop no servidor)
  • Necessário gerir tokens de auth
  • Endpoints PHP a manter
  • Cuidado com quotas agregadas
13

Resumo Geral

Quem faz o quê
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.

14

Cores de Trânsito

Como o rácio define a cor do segmento
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).