DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Consultas geoespaciais com PostGIS

logotech

## PostGIS Desmistificado: Georreferenciando seu Backend com Poder e Precisão

No universo do desenvolvimento de backend, lidamos constantemente com dados. E se esses dados tivessem uma dimensão extra, uma localização geográfica? É aí que entra o PostGIS, uma extensão poderosa para o PostgreSQL que transforma seu banco de dados relacional em um sistema de informações geográficas (SIG) robusto.

Seja para aplicativos de logística, redes sociais com foco em localização, ou qualquer sistema que se beneficie de dados espaciais, dominar o PostGIS é um diferencial. Neste artigo, vamos desbravar como habilitar o PostGIS, armazenar coordenadas de forma eficiente, e realizar queries de distância e intersecção, tudo isso com exemplos práticos em TypeScript/Node.js.

Por que Dados Geográficos são Cruciais?

Imagine um aplicativo de entrega. Saber a localização exata de um restaurante e do cliente é fundamental. Agora, pense em como otimizar rotas, encontrar o estabelecimento mais próximo, ou analisar a densidade de negócios em uma região. Esses são apenas alguns exemplos de como a informação geográfica agrega valor inestimável às aplicações.

O PostGIS nos permite ir além do simples armazenamento de texto ou números. Ele introduz tipos de dados espaciais, como geometry e geography, e um vasto conjunto de funções para manipular e consultar esses dados.

Habilitando a Extensão PostGIS

Antes de tudo, precisamos ter o PostgreSQL instalado. Se você ainda não o tem, a instalação é simples e disponível para a maioria dos sistemas operacionais.

Com o PostgreSQL em funcionamento, o próximo passo é habilitar a extensão PostGIS no seu banco de dados. Isso geralmente é feito conectando-se ao seu banco de dados via psql ou qualquer cliente SQL e executando o seguinte comando:

CREATE EXTENSION postgis;
Enter fullscreen mode Exit fullscreen mode

Esse comando simples é a porta de entrada para um mundo de funcionalidades geoespaciais. Ele adiciona os tipos de dados e funções necessárias ao seu banco de dados.

Armazenando Coordenadas: Pontos de Partida

Para armazenar coordenadas geográficas, o PostGIS oferece os tipos geometry (para dados em um plano euclidiano) e geography (para dados em uma esfera, ideal para coordenadas globais). Para a maioria dos casos de geolocalização, geography é a escolha mais adequada.

Vamos criar uma tabela de exemplo para armazenar locais, incluindo um campo para suas coordenadas:

import { Pool } from 'pg'; // Assumindo o uso da biblioteca 'pg' para Node.js

// Configuração da conexão com o banco de dados
const pool = new Pool({
  user: 'your_db_user',
  host: 'your_db_host',
  database: 'your_db_name',
  password: 'your_db_password',
  port: 5432,
});

interface Location {
  id: number;
  name: string;
  coordinates: { type: string; coordinates: [number, number] }; // Formato GeoJSON para simplicidade
}

async function createLocationsTable() {
  const query = `
    CREATE TABLE IF NOT EXISTS locations (
      id SERIAL PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      geom GEOGRAPHY(Point, 4326) NOT NULL
    );
  `;
  try {
    await pool.query(query);
    console.log('Tabela \"locations\" criada com sucesso ou já existente.');
  } catch (error) {
    console.error('Erro ao criar a tabela \"locations\":', error);
  }
}

async function insertLocation(name: string, longitude: number, latitude: number): Promise<void> {
  // O SRID 4326 é o padrão para coordenadas geográficas (WGS 84)
  const query = `
    INSERT INTO locations (name, geom)
    VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3), 4326)::geography);
  `;
  try {
    await pool.query(query, [name, longitude, latitude]);
    console.log(`Local \"${name}\" inserido com sucesso.`);
  } catch (error) {
    console.error(`Erro ao inserir o local \"${name}\":`, error);
  }
}

// Exemplo de uso:
// createLocationsTable();
// insertLocation('Restaurante Saboroso', -46.6333, -23.5505); // Coordenadas de São Paulo
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • CREATE TABLE IF NOT EXISTS locations: Cria a tabela se ela não existir.
  • id SERIAL PRIMARY KEY: Um identificador único e autoincrementável.
  • name VARCHAR(255) NOT NULL: O nome do local.
  • geom GEOGRAPHY(Point, 4326) NOT NULL: Este é o campo chave.
    • GEOGRAPHY: Indica que estamos usando o tipo de dado geográfico.
    • Point: Especifica que armazenaremos pontos. Outras opções incluem Polygon, LineString, etc.
    • 4326: É o Spatial Reference System Identifier (SRID) para o datum WGS 84, o sistema de coordenadas mais comum usado pelo GPS.
  • ST_SetSRID(ST_MakePoint($2, $3), 4326): Esta é uma função do PostGIS.
    • ST_MakePoint($2, $3): Cria uma representação de ponto a partir das coordenadas de longitude ($2) e latitude ($3). Note que a ordem é longitude, latitude.
    • ST_SetSRID(..., 4326): Associa o SRID 4326 ao ponto criado.
    • ::geography: Converte o ponto com SRID para o tipo geography.

Queries de Distância: Encontrando o Mais Próximo

Uma das operações mais comuns é calcular a distância entre dois pontos. O PostGIS facilita isso com funções como ST_Distance.

async function findNearestLocations(longitude: number, latitude: number, maxDistanceKm: number): Promise<Location[]> {
  const query = `
    SELECT id, name, ST_AsGeoJSON(geom) AS geojson
    FROM locations
    WHERE ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3 * 1000)
    ORDER BY ST_Distance(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography)
    LIMIT 10;
  `;
  // $3 * 1000: A função ST_DWithin espera a distância em metros, então convertemos Km para metros.

  try {
    const result = await pool.query(query, [longitude, latitude, maxDistanceKm]);

    // Mapeia os resultados para o formato esperado da interface Location
    return result.rows.map((row: any) => ({
      id: row.id,
      name: row.name,
      // A função ST_AsGeoJSON retorna a geometria em formato GeoJSON
      coordinates: JSON.parse(row.geojson),
    }));
  } catch (error) {
    console.error('Erro ao buscar locais próximos:', error);
    return [];
  }
}

// Exemplo de uso:
async function exampleNearest() {
  const userLongitude = -46.6500; // Ex: Localização do usuário em São Paulo
  const userLatitude = -23.5600;
  const searchRadiusKm = 5; // Buscar em um raio de 5 Km

  console.log(`Buscando locais próximos a (${userLongitude}, ${userLatitude}) em um raio de ${searchRadiusKm} Km...`);
  const nearest = await findNearestLocations(userLongitude, userLatitude, searchRadiusKm);

  if (nearest.length > 0) {
    console.log('Locais encontrados:');
    nearest.forEach(loc => console.log(`- ${loc.name} (Distância: ${calculateDistanceKm(userLongitude, userLatitude, loc.coordinates.coordinates[0], loc.coordinates.coordinates[1]).toFixed(2)} Km)`));
  } else {
    console.log('Nenhum local encontrado dentro do raio especificado.');
  }
}

// Função auxiliar para calcular distância em Km (apenas para exibição, a consulta SQL já faz isso)
function calculateDistanceKm(lon1: number, lat1: number, lon2: number, lat2: number): number {
    const R = 6371; // Raio da Terra em Km
    const dLat = deg2rad(lat2 - lat1);
    const dLon = deg2rad(lon2 - lon1);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
      Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    const d = R * c; // Distância em Km
    return d;
}

function deg2rad(deg: number): number {
  return deg * (Math.PI / 180);
}

// exampleNearest();
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • ST_DWithin(geom1, geom2, distance_in_meters): Esta função é otimizada para performance. Ela verifica se duas geometrias estão dentro de uma determinada distância uma da outra. É mais eficiente do que calcular a distância e depois comparar, pois pode usar índices espaciais para acelerar a busca.
    • Estamos convertendo maxDistanceKm para metros multiplicando por 1000.
  • ST_Distance(geom1, geom2): Calcula a distância exata entre duas geometrias. Usamos isso na cláusula ORDER BY para ordenar os resultados pela proximidade.
  • ST_AsGeoJSON(geom): Converte a geometria armazenada de volta para o formato GeoJSON, que é amplamente utilizado em aplicações web e APIs.

Queries de Intersecção: Encontrando o que se Sobrepõe

Além de distâncias, podemos querer saber quais locais estão dentro de uma determinada área (por exemplo, um polígono de um bairro) ou quais áreas se sobrepõem. Funções como ST_Intersects, ST_Contains, ST_Within, e ST_Overlaps são extremamente úteis aqui.

Vamos supor que temos uma tabela de bairros com geometrias poligonais e queremos encontrar todos os locais que estão dentro de um bairro específico.

Primeiro, a criação da tabela de bairros:

async function createNeighborhoodsTable() {
  const query = `
    CREATE TABLE IF NOT EXISTS neighborhoods (
      id SERIAL PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      geom GEOGRAPHY(Polygon, 4326) NOT NULL
    );
  `;
  try {
    await pool.query(query);
    console.log('Tabela \"neighborhoods\" criada com sucesso ou já existente.');
  } catch (error) {
    console.error('Erro ao criar a tabela \"neighborhoods\":', error);
  }
}

async function insertNeighborhood(name: string, polygonGeoJSON: string): Promise<void> {
  const query = `
    INSERT INTO neighborhoods (name, geom)
    VALUES ($1, ST_SetSRID(ST_GeomFromGeoJSON($2), 4326)::geography);
  `;
  // ST_GeomFromGeoJSON converte uma string GeoJSON em uma geometria PostGIS
  try {
    await pool.query(query, [name, polygonGeoJSON]);
    console.log(`Bairro \"${name}\" inserido com sucesso.`);
  } catch (error) {
    console.error(`Erro ao inserir o bairro \"${name}\":`, error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora, a função para encontrar locais dentro de um bairro:

async function findLocationsInNeighborhood(neighborhoodName: string): Promise<Location[]> {
  const query = `
    SELECT l.id, l.name, ST_AsGeoJSON(l.geom) AS geojson
    FROM locations l
    JOIN neighborhoods n ON ST_Intersects(l.geom, n.geom)
    WHERE n.name = $1;
  `;
  // ST_Intersects retorna true se as geometrias se tocam em qualquer ponto (incluindo bordas)

  try {
    const result = await pool.query(query, [neighborhoodName]);

    return result.rows.map((row: any) => ({
      id: row.id,
      name: row.name,
      coordinates: JSON.parse(row.geojson),
    }));
  } catch (error) {
    console.error(`Erro ao buscar locais no bairro \"${neighborhoodName}\":`, error);
    return [];
  }
}

// Exemplo de uso:
async function exampleIntersection() {
  // await createNeighborhoodsTable();
  // Exemplo de GeoJSON para um polígono simples (simplificado para ilustração)
  // const saoPauloBairroGeoJSON = '{\"type\":\"Polygon\",\"coordinates\":[[[-46.6500,-23.5500],[-46.6300,-23.5500],[-46.6300,-23.5700],[-46.6500,-23.5700],[-46.6500,-23.5500]]]}';
  // await insertNeighborhood('Exemplo Bairro', saoPauloBairroGeoJSON);

  // Certifique-se que 'locations' e 'neighborhoods' tenham dados e que haja sobreposição
  const neighborhoodNameToSearch = 'Exemplo Bairro';
  console.log(`Buscando locais no bairro \"${neighborhoodNameToSearch}\"...`);
  const locationsInNeighborhood = await findLocationsInNeighborhood(neighborhoodNameToSearch);

  if (locationsInNeighborhood.length > 0) {
    console.log(`Locais encontrados em \"${neighborhoodNameToSearch}\":`);
    locationsInNeighborhood.forEach(loc => console.log(`- ${loc.name}`));
  } else {
    console.log(`Nenhum local encontrado em \"${neighborhoodNameToSearch}". Verifique se os dados e a geometria do bairro estão corretos.`);
  }
}

// exampleIntersection();
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • ST_GeomFromGeoJSON($2): Converte uma string no formato GeoJSON para um objeto de geometria que o PostGIS entende.
  • ST_Intersects(geom1, geom2): Retorna true se as duas geometrias tiverem qualquer ponto em comum. Para polígonos, isso significa que eles se tocam ou se sobrepõem.
  • JOIN ... ON ST_Intersects(...): Usamos um JOIN condicional à intersecção das geometrias para filtrar os locais que estão dentro do polígono do bairro.

Considerações de Performance e Boas Práticas

  1. Índices Espaciais: Para consultas eficientes em grandes volumes de dados, é essencial criar índices espaciais nos seus campos geography ou geometry. Use CREATE INDEX index_name ON table_name USING GIST (geom);. O GIST (Generalized-Search Tree) é o tipo de índice mais comum e eficiente para dados espaciais no PostGIS.

  2. geography vs. geometry: Como mencionado, use geography para dados globais e cálculos de distância/área que precisam ser precisos na superfície da Terra. Use geometry para dados locais onde a curvatura da Terra pode ser ignorada e você precisa de performance máxima ou trabalha em um sistema de coordenadas projetadas específico.

  3. SRID Consistente: Mantenha a consistência no uso do SRID (Spatial Reference System Identifier). O SRID 4326 é o padrão global para GPS.

  4. Funções Otimizadas: Prefira funções como ST_DWithin em vez de calcular a distância e depois filtrar, pois elas são otimizadas para usar índices espaciais.

  5. Tipagem Forte (TypeScript): Utilize interfaces e tipos para garantir a integridade dos dados que entram e saem do seu banco de dados, como mostrado nos exemplos com Location.

Conclusão

O PostGIS é uma ferramenta transformadora para qualquer backend que lide com dados geográficos. Ao habilitar a extensão, armazenar coordenadas corretamente e utilizar as ricas funções espaciais disponíveis, você pode construir funcionalidades poderosas como geolocalização, otimização de rotas, análises de proximidade e muito mais.

Com exemplos práticos em TypeScript/Node.js, esperamos ter desmistificado o processo e incentivado você a explorar o potencial geoespacial do PostgreSQL. Lembre-se sempre da importância dos índices espaciais para garantir a performance de suas consultas.

Continue explorando as vastas capacidades do PostGIS – há um mundo inteiro de dados geoespaciais esperando para ser descoberto e utilizado em suas aplicações!

Top comments (0)