Se você já quis desenvolver uma aplicação web com dados geográficos vetoriais, provavelmente precisou utilizar um servidor como o GeoServer, que pode ter instalação trabalhosa e consumo excessivo de recursos da máquina, especialmente se ela for um VPS de configurações mais limitadas. Não é objetivo deste texto em momento algum desincentivar a utilização do GeoServer, afinal é um servidor robusto com muitas funcionalidades de disponibilização de dados geográficos nos mais diversos formatos.

O objetivo é apresentar uma alternativa mais enxuta (porém poderosa) e de configuração mais simples para começar a desenvolver suas aplicações baseadas em dados de um banco PostgreSQL com extensão PostGIS. O pg_featureserv surge como uma alternativa interessante, pois é um servidor de feições geográficas armazenadas no PostGIS, escrito em Go e com configuração e execução inicial rápida. É disponibilizado pela CrunchyData e seu código e instruções de instalação podem ser encontrados no Github.

Campina Grande, no interior paraibano, é palco do Maior São João do Mundo numa festa que dura mais de mês, e aproveitando que esse período se aproxima, uso a cidade como estudo de caso de transporte público a partir de dados de sistema viário, bairros e paradas de ônibus obtidos do projeto de mapeamento colaborativo OpenStreetMap e do projeto Observa Campina Grande. Uso também as coordenadas geográficas de endereços do último censo disponibilizadas pelo IBGE que representam por meio de pontos os domicílios e estabelecimentos onde houve coleta de de dados.

A pergunta principal é: quão bem atendidos por paradas de transporte público são os domicílios de bairros da cidade? Responderemos ela a partir de operações especiais realizadas com PostGIS e resultados obtidos por meio do pg_featureserv.

Eu assumo que você:

  • tenha conhecimento básico a intermediário de SQL
  • tenha conhecimento básico de operações com dados geoespaciais
  • tenha PostgreSQL 9.5+ e PostGIS 2.4+ instalados
  • tenha o ambiente de execução de Go 1.13+ instalado na sua máquina

Estrutura do banco

Vias

Contém nome, tipo de superfície, geometria e outros campos úteis. Aqui são utilizadas apenas para facilitar a visualização dos dados no mapa. Porém, para planejamento urbano e simulação de tráfego são de extrema importância. Dados obtidos do OpenStreetMap.

Bairros

Contém nome, geometria e zona onde os bairros se encontram. Dados da Secretaria de Planejamento de Campina Grande de 2021 obtidos pelo Observa Campina Grande.

Paradas de ônibus

Contém nome e geometria. Dados de fev/24 obtidos do OpenStreetMap/STTP.

Coordenadas dos endereços

Contém tipo de endereço e outros campos úteis. Filtragem realizada para obtenção de apenas domicílios (particulares ou coletivos). Dados obtidos do IBGE.

Na figura abaixo, um mapa mostra as camadas disponíveis no banco:

Mapa da zona urbana do município de Campina Grande, na Paraíba. Em cor cinza claro com bordas pretas grossas estão representados os bairros. Em linhas finas de cor cinza o sistema viário. Pontos de cor azul claro neon com borda preta representam paradas de ônibus e pontos de cor vermelha com borda pra representam endereços (domicílios ou estabelecimentos) coletados no Censo. Há muito mais pontos vermelhos do que azuis. No canto inferior direito há a legenda explicando os dados e no canto superior esquerdo uma escala dividida de quilômetro em quilômetro com uma seta de norte em vermelho claro.

Primeiros passos

O primeiro passo do nosso trabalho é criar o banco de dados e habilitar a extensão PostGIS nele:

CREATE DATABASE campinagrande;
CREATE EXTENSION postgis;

O banco de dados está disponível neste repositório se você quiser restaurá-lo na sua máquina. Ele é um arquivo geopackage e você pode importá-lo para o seu banco utilizado o canivete suíço da conversão de formatos de dados geoespaciais: ogr2ogr.

ogr2ogr -f PostgreSQL "PG:user=elmo password=minhasenha host=localhost dbname=campinagrande" CampinaGrande.gpkg

O banco utilizado tem o propósito de teste e qualquer alteração acidental não trará prejuízos, contudo, é uma boa prática criar usuários com privilégios mais restritivos ao banco, que permitam apenas leitura de dados. Podemos criar um usuário para utilização pelo pg_featureserv com o seguinte código:

CREATE USER featureserver;
GRANT USAGE ON SCHEMA public TO featureserver;
GRANT SELECT ON TABLE public.bairros TO featureserver;
GRANT SELECT ON TABLE public.paradas_de_onibus TO featureserver;
GRANT SELECT ON TABLE public.coords_censo_22 TO featureserver;
GRANT SELECT ON TABLE public.vias TO featureserver;

Vamos criar também a extensão unaccent para que seja possível a busca por nomes de bairros sem a preocupação de verificar acentuação ou capitalização. Também vamos criar o schema testes onde nossas funções no PostgreSQL serão armazenadas e acessadas pelo pg_featureserv.

CREATE EXTENSION unaccent;
CREATE SCHEMA testes;

O executável do pg_featureserv adequado para o seu sistema operacional pode ser transferido por aqui, caso ainda não o tenha feito. Após a extração do arquivo transferido, verifique que na pasta config há um arquivo pg_featureserv.toml.example. Copie ele e cole na mesma pasta, removendo a parte .example do arquivo. O arquivo de configuração pg_featureserv.toml será lido quando executarmos o pg_featureserv pela linha de comando.

Mas antes de iniciarmos a execução do servidor, algumas configurações:

Descomente a linha que começa com DbConnection e altere o valor dela substituindo pelos dados que identificam o seu banco. Procure também a linha FunctionIncludes e substitua o schema2 por testes.

DbConnection = "postgresql://elmo:minhasenha@localhost/campinagrande”
FunctionIncludes = [ "postgisftw", "testes" ]

Essa configuração é essencial, mas vale destacar outras que também são interessantes:

HttpPort = 9000 #você pode mudar para alguma outra porta da sua preferência
LimitDefault = 20 # esse é o limite padrão de feições retornadas por consultadas
# é interessante colocar um número mais alto, entre 50 e 100
BasemapUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" 
# aqui você pode configurar outro mapa base para a visualização de seus dados.
# uso uma conta da Thunderforest com minha chave de API
# para poder utilizar mapa base voltado ao tema de transporte

Lista de servidores de tiles baseados em dados do OSM

Depois de salvas as configurações no arquivo, você pode voltar pra pasta raiz da extração e executar o seguinte comando para permitir a execução do pg_featureserv ao seu usuário (ou simplesmente executar como administrador no Windows):

chmod u+x pg_featureserv
./pg_featureserv

Se as configurações estiverem corretas e a conexão com o banco foi estabelecida, você deve ver no terminal algo parecido com isso:

INFO[0000] ----  pg_featureserv - Version 1.3.1 ----------
INFO[0000] Using config file: /home/elmo/pg_featureserv/config/pg_featureserv.toml
INFO[0000] Using database connection info from config file
INFO[0000] Connected as elmo to campinagrande @ localhost
INFO[0000] Serving HTTP  at http://0.0.0.0:9000
INFO[0000] CORS Allowed Origins: *
INFO[0000] ====  Service: pg-featureserv  ====

Interface

Acessando o pg_featureserv em http://localhost:9000 (ou outra porta definida por você), é mostrada esta tela inicial na qual é possível acessar dados sobre a API subjacente, as coleções (relações, views) e as funções que estão dentro dos schemas que autorizamos acesso lá no arquivo toml de configuração.

Tela inicial do pg_featureserv mostrando OpenAPI schema, conformance, collections e functions. Botão de JSON ao lado direito de cada uma delas.

Acessando collections, podemos encontrar todas as tabelas no nosso schema public de acesso autorizado ao pg_featureserv.

Tela do pg_featureserv mostrando as collections disponíveis. Tabelas de bairros, coordenadas de endereços, paradas de ônibus e vias. Todas tem dois botões do lado direito: View e JSON.

Acessando a tabela paradas_de_onibus, visualizamos no mapa o ponto associado a cada uma das paradas com estilo de preenchimento de cor vermelha. Se você quiser, pode personalizar cores e layouts de apresentação dos dados, pode alterar o código CSS/Javascript dos arquivos na pasta assets. Caso você esteja vendo poucas feições, altere o limite no canto superior esquerdo para um número maior e refaça a consulta. Clicando em JSON, você tem acesso à URL que retorna os mesmos dados em formato JSON. Esses dados podem ser baixados ou carregados na sua aplicação Web, por exemplo.

Tela do pg_featureserv mostrando a visualização dos dados da tabela paradas de ônibus. Os pontos representando paradas de ônibus tem cor vermelha e estão sobre um mapa base de alto contraste destacando feições de transporte público. No canto superior esquerdo há uma caixa para passagem de argumentos à consulta.

Primeira função

O código abaixo cria a função paradas_no_bairro dentro do schema testes que autorizamos que o pg_featureserv utilize. A função recebe uma string chamada nome, contendo nome do bairro. Busca dentro da tabela de bairros um deles cujo nome seja igual à string recebida, desconsiderando acentuação e capitalização.

Exemplo: a busca por catole retorna o bairro Catolé, mas a busca catol não retorna Catolé. Da mesma forma, a busca por bodocongo retorna Bodocongó, mas não retorna Novo Bodocongó. Numa aplicação real, seria interessante implementar uma busca mais flexível no front-end e buscar o bairro alvo no pg_featureserv apenas por um identificador. A função de busca foi simplificada a fim de exercício.

A outra condição é que serão retornadas apenas as paradas de ônibus geometricamente contidas na geometria associada ao bairro. Os campos retornados são o identificador local, o identificador OSM, o nome e a geometria (ponto) das paradas. O pg_featureserv se encarrega de fazer a conversão dos dados para o formato GeoJSON.

CREATE OR REPLACE FUNCTION testes.paradas_no_bairro(nome text)
RETURNS TABLE(id integer, full_id text, name text, geom geometry)
as $$
BEGIN
	RETURN query
	SELECT
	pdo.id::integer AS id, pdo.full_id::text AS full_id,
	pdo.name::text AS name, pdo.geom AS "geom"
	FROM bairros b JOIN paradas_de_onibus pdo
	ON ST_Contains(b.geom, pdo.geom)
	WHERE unaccent(b.name) ILIKE unaccent(nome);
END;
$$
LANGUAGE 'plpgsql' STABLE PARALLEL SAFE;

Interessante é que se você quiser reaproveitar essa função no GeoServer, você pode simplesmente criar uma view dentro de um espaço de trabalho com o armazenamento que contenha seu banco de dados. O código para criar a view na interface do GeoServer é esse:

SELECT * FROM testes.paradas_no_bairro('%nome%');

No GeoServer, em um espaço de trabalho chamado tutoriais com uma view nomeada paradas_no_bairro, é possível acessar o resultado em formato GeoJSON da busca por bairro com nome bodocongo numa instalação local padrão do GeoServer:

http://localhost:8080/geoserver/wfs?service=WFS&version=1.0&request=GetFeature&typeName=tutoriais:paradas_no_bairro&outputformat=application/json&viewparams=nome:bodocongo

Para acessar a interface de consulta e visualização da view que acabamos de criar, acesse a página inicial do pg_featureserv e navegue até encontrá-la ou simplesmente acesse a URL: http://localhost:9000/functions/testes.paradas_no_bairro/items.html

Altere o limite para 100 feições retornadas e preencha o campo nome em Function Args com o nome completo de um bairro, como exemplo bodocongo, para buscar todas as paradas dentro do bairro Bodocongó. Ou acesse o resultado pela URL: http://localhost:9000/functions/testes.paradas_no_bairro/items.html?nome=bodocongo&limit=100

Tela do pg_featureserv mostrando a visualização da consulta por paradas de ônibus dentro de um bairro. Os pontos representando paradas de ônibus tem cor vermelha e estão sobre um mapa base de alto contraste destacando feições de transporte público. No canto superior esquerdo há uma caixa para passagem de argumentos à consulta. O campo nome está preenchido com a palavra bodocongo e o campo limite com o valor 100

Se houvesse mais de 100 paradas de ônibus no bairro, apenas as 100 primeiras seriam retornadas. Como no bairro Bodocongó há menos de 100, apenas as 84 geometrias dentro da geometria deste bairro foram retornadas.

Segunda Função

Já a outra função que vamos implementar tem alguns passos a mais. O objetivo é criar uma geometria resultado da união de vários buffers, cada buffer tendo como centro uma das paradas disponíveis e raio arbitrário definido em metros. A partir dessa união de buffers, a interseção com cada geometria de bairro será feita para obter a área de cobertura de paradas dentro do bairro. A saída da função é a quantidade de domicílios atendidos, a quantidade de domicílios não-atendidos e o percentual de atendimento. A figura abaixo explica visualmente nosso objetivo:

pdo_buffers

300 metros é uma distância possível de ser caminhada com tranquilidade dentro de 5 minutos para pessoas sem qualquer redução de mobilidade e em aproximadamente 10 minutos para pessoas com redução de mobilidade, como pessoas idosas e/ou em cadeira rodas. Note que a criação de um buffer é uma aproximação que não necessariamente reflete a realidade, pois as ruas formam esquinas e curvas que aumentam a distância do caminho estimado em linha reta. O ideal é que sejam calculadas curvas de alcance (ou isócronas) que são estimativas mais próximas da realidade por considerarem a não-linearidade de trajetos. Veja a comparação de regiões alcançáveis em até 500 metros usando a técnica de buffer e a técnica de cálculo de alcance.

explicacao2

Contudo, o objetivo do trabalho é mostrar a utilização do pg_featureserv e por isso, o cálculo de curvas isócronas fica para outro texto. Mas fica registrado que também é possível fazer esse cálculo com PostGIS com auxílio da extensão pgRouting. :)


Para acelerar consultas futuras, vamos criar uma coluna chamada qtde_domicilios em cada bairro para que ela seja calculada só uma vez visto que ela provavelmente só será recalculada em caso de atualização de geometria de bairro ou em próxima atualização de dados do Censo disponibilizados pelo IBGE. Criamos a coluna com o código seguinte:

ALTER TABLE bairros ADD qtde_domicilios integer;

Populamos a coluna com o seguinte código:

UPDATE bairros b
SET qtde_domicilios =
(
	SELECT COUNT(*)
	FROM bairros b1, coords_censo_22 cc22
	WHERE ST_Contains(b1.geom, cc22.geom)
	AND b.id = b1.id
)

Agora, criaremos uma view materializada que irá retornar todos os domicílios (pontos) do bairro que estão dentro de um buffer de 300 metros a partir do ponto da parada. É possível criar várias views com distâncias diferentes. Mas por que uma view materializada? Porque são muitas operações geométricas com alto custo de processamento. Ao invés de recalcularmos sempre que precisarmos destes dados, calculamos uma vez e o cálculo só é refeito quando houver uma ou mais atualizações de geometrias.

DROP MATERIALIZED VIEW bairro_domicilio_300m_parada CASCADE;

CREATE MATERIALIZED VIEW bairro_domicilio_300m_parada AS
WITH buffer_total AS
(
	SELECT ST_Union(ST_Buffer(pdo.geom, 300)) AS geom
	FROM paradas_de_onibus pdo
)

SELECT bairro_buffer.bairro_id, COUNT(*) AS qtde_domicilios
FROM coords_censo_22 cc22,
(
	SELECT b.id AS bairro_id, ST_Intersection(b.geom, buffer_total.geom) AS geom
	FROM bairros b, buffer_total
) bairro_buffer
WHERE ST_Contains(bairro_buffer.geom, cc22.geom)
GROUP BY bairro_buffer.bairro_id

A função que será utilizada pelo pg_featureserv é criada com o seguinte código:

CREATE OR REPLACE FUNCTION testes.percentual_atendimento_onibus()
RETURNS TABLE(name text, geom geometry, domicilios_atendidos integer, domicilios_nao_atendidos integer, percentual_atendido float)
as $$
BEGIN
	RETURN query
	SELECT
		b.name::text,
		b.geom,
		bdmp.qtde_domicilios::integer as domicilios_atendidos,
		(b.qtde_domicilios - bdmp.qtde_domicilios)::integer as domicilios_nao_atendidos,
		round(((bdmp.qtde_domicilios::float/b.qtde_domicilios::float)*100)::numeric, 2)::float as percentual_atendido
	FROM bairros b JOIN bairro_domicilio_300m_parada bdmp ON b.id = bdmp.bairro_id 
	ORDER BY percentual_atendido ASC, domicilios_atendidos ASC;
END;
$$
LANGUAGE 'plpgsql' STABLE PARALLEL SAFE;

O resultado da consulta pode ser acesso no endereço e visualizado no navegador: http://localhost:9000/functions/testes.percentual_atendimento_onibus/items.html?limit=100

Ou pode ser baixado como um arquivo GeoJSON: http://localhost:9000/functions/testes.percentual_atendimento_onibus/items.json?limit=100

O GeoJSON retornado pode ser mostrado num webmapa cloroplético, mas para simplificar, baixei o GeoJSON e o carreguei no QGIS, aplicando um estilo graduado.

atendimento_onibus_cg

Observando o mapa, vemos que a região central da cidade tem alta cobertura (quando não total) de atendimento de paradas de ônibus a 300 metros de distância dos domicílios. Os extremos Sul e Oeste, mesmo com percentuais menores, continuam bem atendidos. Não é a mesma configuração das regiões Norte e Leste. Contudo, deve-se analisar também outras características dos bairros, usando como base dados socioeconômicos, para melhores análises. No extremo leste: o bairro Ronaldo Cunha Lima, com o menor percentual de atendimento possui maioria dos domicílios em condomínio fechado, já o bairro Tropeiros da Borborema, segundo menos atendido, tem apenas 22 domicílios mapeados.

Os 3 bairros mais bem atendidos:

Bairro % atendimento Domicílios atendidos
Catolé 100 9479
Liberdade 100 5990
Centro 100 5249

Os 3 bairros menos atendidos:

Bairro % atendimento Domicílios atendidos
Ronaldo Cunha Lima 14.83 31
Tropeiros da Borborema 27.27 6
Jardim Itararé 36.18 292

A tabela completa pode ser acessada aqui.

Considerações finais

  • O pg_featureserv tem muito mais funcionalidades do que as aqui descritas, fica a critério da sua curiosidade investigar.

  • PostGIS e pgRouting são ótimas ferramentas para análise de dados sobre mobilidade urbana

  • Com o aumento do tamanho dos buffers, para 400 ou 500 metros, por exemplo, os percentuais de atendimento melhoram. Contudo, curvas de alcance e isócronas continuam representando melhor a realidade dos deslocamentos.

  • A análise da distribuição de paradas de ônibus responde perguntas apenas sobre tempo/distância necessários para acessar instalações de transporte público. Nada diz sobre conectividade. Exemplo: É possível sair do bairro A e chegar até o bairro C diretamente ou por alguma conexão próxima em B? Para responder este tipo de pergunta, entram em jogo dados sobre rotas de ônibus.

  • A fogueira está queimando, Meu Querido São João! 🪗

Obrigado por ler até aqui :)

Foto de capa do Thiago Japyassu