Diagrama de Blocos — Documentação técnica¶
Funcionalidade do plugin SIG-Bus que visualiza, de forma interativa, a distribuição temporal das viagens de uma ou mais linhas e a alocação de frota (blocos) inferida a partir do GTFS.
- Autor: Diego Camargo · Versão da feature: 1 (Modo Viagens + Modo Blocos)
- Entrada na UI: diálogo do SIG-Bus → botão “Diagrama de Blocos”
- Documentos de projeto:
PLANEJAMENTO_DIAGRAMA.mdeARQUITETURA_DIAGRAMA.md(na raiz do repositório de trabalho)
1. Motivação (aspecto de transportes)¶
O planejamento operacional de transporte público trabalha com três objetos encadeados:
- Viagem (trip) — uma realização orientada de uma linha, de um terminal a outro, com horários de partida/chegada em cada parada.
- Bloco (block) — a sequência de viagens executadas por um mesmo veículo ao longo do dia, da saída à recolha na garagem. É o que dimensiona a frota.
- Headway — o intervalo entre partidas sucessivas de uma mesma linha/sentido; é a frequência que o passageiro efetivamente sente no ponto.
Um diagrama de blocos (também Marey chart / gráfico tempo–veículo) expõe esses três de uma vez: no eixo X o tempo do dia; no eixo Y as viagens organizadas por linha/sentido (Modo Viagens) ou por veículo (Modo Blocos). Permite ler picos, ociosidade, encadeamento e tamanho de frota — dados que um mapa geográfico não mostra.
1.1 A restrição central: o feed não tem block_id¶
No GTFS, o campo que diz qual veículo opera quais viagens é trips.block_id. O feed da
BHTrans usado no projeto não traz esse campo (trips.txt =
route_id, service_id, trip_id, trip_headsign, direction_id, shape_id).
Consequência de projeto: não há blocos para ler — eles são inferidos. Isso define os dois modos da ferramenta:
| Modo | O que mostra | Natureza |
|---|---|---|
| Viagens | Faixa por (linha, sentido); uma barra por viagem | Determinístico (lê o GTFS) |
| Blocos | Faixa por veículo inferido; gaps de ociosidade | Estimado (heurística) |
O Modo Blocos é rotulado “estimado” na UI: é uma reconstrução plausível da escala, não a programação oficial da operadora.
2. Conceitos de transporte usados no código¶
| Conceito | Campo GTFS / derivação | Onde aparece |
|---|---|---|
| Linha | routes.route_short_name (ex.: 101) |
chave de seleção e join |
| Sentido | trips.direction_id (0=ida, 1=volta) |
faixa / espessura da barra |
| Tipo de dia | trips.service_id |
filtro; agrupa a inferência |
| Início da viagem | MIN(stop_sequence) → departure_time |
barra (X inicial) |
| Fim da viagem | MAX(stop_sequence) → arrival_time |
barra (X final) |
| Terminais | stop_id da 1ª/última parada |
casamento no encadeamento |
| Layover | gap entre fim de uma viagem e início da próxima | parâmetro do bloco |
| Deadhead | reposicionamento sem passageiros (terminais diferentes) | parâmetro do bloco |
| Headway | start(viagem) − start(viagem anterior da mesma linha+sentido) |
indicador na seleção |
Horários ≥ 24h: o GTFS representa serviço pós-meia-noite como
25:30:00. Todo o código converte horário em segundos desde 00:00 (parse_gtfs_time), nunca tratando como relógio — ordenar/encadear por string quebraria a virada do dia.
3. Arquitetura (aspecto de código)¶
Separação em três camadas, padrão MVC — o que torna a futura edição (arrastar viagens entre veículos) uma extensão, não uma reescrita.
Interface (Controller) block_diagram_dialog.py → orquestra; UI em Python puro
│ params ▲ render(Schedule)
▼ │
Core (Model) block_core.py → leitura, modelo, inferência (QgsTask)
│ lê │
▼ │
feed.gpkg (SQLite) Engine (View) block_scene.py + block_view.py
→ QGraphicsScene de itens clicáveis
3.1 Arquivos¶
| Arquivo | Camada | Responsabilidade |
|---|---|---|
block_core.py |
Core | Trip/Block/BlockParams/Schedule, ScheduleReader, BlockBuilder, BlockDiagramTask |
schedule_edit_core.py |
Core | Manipulação de grade de horários temporária para o ajuste fino (reaproveitamento da engine) |
schedule_table_core.py |
Core | Núcleo puro da tabela de horários (Stops x Trips), formatação e manipulação de matrizes de horários |
schedule_grid_widget.py |
Interface | ScheduleGridWidget: a tabela de horários em si (QTableWidget paradas × viagens) e o collect_changes() que devolve só as células editadas |
schedule_editor_widget.py |
Interface | ScheduleEditorWidget: o editor de horários das duas telas — a página "Horários" do assistente e a janela "Ajustar horários" da aba "Edição GTFS" — com a matriz (ScheduleGridWidget) e o diagrama (BlockView/BlockScene) empilhados num QSplitter vertical (decisões 155-157) sobre o mesmo rascunho de stop_times; changed_rows()/diff_stop_times decide o que grava |
block_scene.py |
Engine | BlockScene, TripItem, layout, cores, headway, departure_ticks (régua de saídas) |
block_view.py |
Engine | BlockView: zoom, pan, export PNG/SVG, preservação de enquadramento pós-redesenho |
block_diagram_dialog.py |
Interface | janela standalone, controles, painel de detalhes |
SigBus_dialog.py |
Integração | botão “Diagrama de Blocos” + diagramaClicked() |
3.2 Por que QGraphicsView/QGraphicsScene¶
- Interação nativa: clique, hover, seleção, zoom e pan (e drag para a edição futura).
- Zero dependências — Qt está sempre no QGIS; matplotlib não está disponível
nesta instalação (o relatório do plugin já usa
QPainterpelo mesmo motivo). - Exporta para PNG (
QImage) e SVG (QSvgGenerator) com o mesmo código.
4. Camada de dados (block_core.py)¶
4.1 Modelo¶
Trip— viagem atômica:trip_id, route_short_name, direction_id, service_id, shape_id, start_time_s, end_time_s, start_stop_id, end_stop_id, n_stops, block_id.Block—block_id+ lista deTrip; propriedadesspaneidle_seconds.Schedule— objeto que a Engine renderiza:trips,blocks,fleet_size,mode,warnings;time_boundscalcula os limites do eixo X com folga.
4.2 Leitura (ScheduleReader)¶
Lê via sqlite3 direto no GeoPackage (mesmo padrão de _AlocacaoTask). Pontos críticos:
stop_times.txttem ~136 MB → a leitura é restrita às linhas selecionadas (route_id IN (...)) e usa os índicesidx_st_tripcriados porcreate_join_indexes(). Nunca varre a tabela inteira nem itera feição-a-feição.- A primeira/última parada de cada viagem é obtida pela ordem canônica
(
stop_sequence), não pelo horário — robusto a dados fora de ordem. route_short_name → route_idpode ser 1→N (a mesma linha tem váriosroute_id); oIN (...)cobre todos.
4.3 Inferência de blocos (BlockBuilder)¶
Heurística gulosa de frota mínima (problema de cobertura de viagens por veículos):
para cada viagem (em ordem de início):
candidatos = veículos livres v tais que
gap = trip.start − v.free
gap ≥ layover_mín
E (relaxado OU gap ≤ layover_máx)
E (deadhead/relaxado OU v.local == trip.terminal_origem)
se há candidatos: escolhe o de MAIOR v.free (menos ocioso)
senão: abre um veículo novo
atualiza v: local = trip.terminal_destino, free = trip.fim
nº de veículos = nº de blocos
Características de transporte embutidas:
- Encadeamento por
service_id— não mistura dia útil com domingo (assinaturas de frota distintas). - Cruza linhas — um veículo pode passar da linha 101 para a 102 se o terminal casar (frota compartilhada); é o ganho do modo multi-linha.
- Parâmetros (
BlockParams):layover_min_s(5 min),layover_max_s(45 min),allow_deadhead(encadeia terminais diferentes),relaxed(ignora o teto de layover → frota mínima teórica, limite inferior).
A frota = len(blocks). Com vários serviços selecionados, a contagem é somada por
serviço e um aviso recomenda escolher um único serviço para o número real.
4.4 Concorrência (BlockDiagramTask)¶
Subclasse de QgsTask (padrão _GtfsLoadTask/_AlocacaoTask): I/O pesado em run()
(thread de fundo), entrega do Schedule em finished() (thread da GUI) via sinais
finishedOk/failed. A GUI nunca congela.
5. Engine gráfica (block_scene.py, block_view.py)¶
5.1 Mapeamento e layout¶
TimeAxisMapperconverte tempo↔X e índice de faixa→Y.- Empacotamento em sub-linhas (
_assign_rows): dentro de uma faixa, viagens que se sobrepõem no tempo vão para sub-linhas distintas (greedy interval packing). Sem isso, viagens simultâneas ficariam “encavaladas” e não clicáveis. No Modo Viagens, o nº de sub-linhas no pico de uma faixa ≈ viagens simultâneas daquele sentido.
5.2 Codificação visual¶
| Atributo visual | Modo Viagens | Modo Blocos |
|---|---|---|
| Faixa (eixo Y) | (linha, sentido) — 101 ▸ ida |
veículo — V1 · 101 |
| Cor da barra | por linha | por veículo (matizes pelo ângulo áureo) |
| Espessura | ida cheia, volta fina | idem |
| Conectores pontilhados | — | ociosidade entre viagens do veículo |
| Traço curto no pé do eixo | régua de saídas: 1 traço por partida, ida em cima / volta embaixo | idem |
A cor por veículo usa rotação de matiz de 137,5° (QColor.fromHsv) para manter cores
bem distintas mesmo com dezenas de veículos.
5.3 Indicador de headway (seleção)¶
Ao clicar numa viagem — nos dois modos —, desenha-se uma cota de desenho técnico
ligando o início da viagem anterior da mesma linha+sentido ao início da viagem
selecionada: linha de cota horizontal acima das duas barras, linhas de chamada
verticais descendo até cada início, traços nas duas pontas e o rótulo N min
centralizado — numa cota o que se lê é a medida; o que ela mede (o intervalo entre
inícios da mesma linha e sentido) já está dito pela geometria. A cota é sempre reta, mesmo quando as duas viagens estão em sub-linhas
diferentes (antes era uma diagonal entre os centros das barras). A viagem anterior é
pré-computada em _prev_trip (agrupando por (linha, sentido) e ordenando por início);
a primeira viagem de cada linha/sentido não tem headway.
5.3.1 Seleção de extremo¶
TripItem.mousePressEvent escolhe, além da viagem, o extremo mais próximo do X
clicado: metade esquerda → 'first' (saída), metade direita → 'last' (chegada).
BlockScene.select_trip_endpoint(item, endpoint) guarda o extremo em
selected_endpoint e emite endpointClicked(Trip, str), ao lado do tripClicked já
existente — o diálogo standalone do Diagrama de Blocos continua funcionando sem conectar
nada novo. Quem usa isso é a etapa de ajuste de horários da aba "Construir GTFS", que
precisa saber qual ponta mover com >/<.
5.4 Interação (block_view.py)¶
- Roda do mouse → zoom (âncora sob o cursor); botão do meio → pan.
- Teclado:
>,<,+e-emitemnudgeKeyPressed(str)com o caractere. A leitura é porevent.text()(o layout já resolveu a tecla —>e<ficam em teclas diferentes em ABNT2 e US-International), comKey_Plus/Key_Minusdo teclado numérico aceitos em adição. A view continua sem conhecer o modelo: quem traduz tecla em deslocamento de horário é o diálogo. fit_all()enquadra a cena inteira ao gerar — essencial porque uma faixa de ida muito alta (linha movimentada) escondia a faixa de volta abaixo da tela.- Preservação de enquadramento (zoom/pan, decisões 109-111):
viewport_state()guarda a transformação e o centro em coordenadas de cena;restore_viewport()os reaplica depois de um redesenho. Quem decide entre preservar e enquadrar é o chamador — o primeiro desenho e o "Restaurar frequência regular" chamamfit_all(), os demais preservam, e o botão Enquadrar tudo reenquadra sob demanda. - Export PNG (
QImage, 2×) e SVG (QSvgGenerator, seQtSvgpresente).
5.5 Régua de saídas¶
No pé do eixo de tempo, abaixo da linha de base, um traço vertical curto por partida: banda de cima = ida, banda de baixo = volta (rotuladas na faixa de rótulos, e só quando há traço na banda). É o equivalente a um rug plot sob um eixo de tempo — a mancha de traços mostra sozinha onde está o pico e onde está o vale de saídas, por sentido, sem varrer o diagrama faixa por faixa.
departure_ticks(trips) (função pura de módulo) devolve (start_time_s, banda) por
viagem, ordenado por horário; banda é 'volta' só quando direction_id == '1' —
qualquer outro valor, inclusive vazio, é 'ida'. _draw_departure_rug desenha a partir
disso, e a altura reservada é RUG_H (constantes RUG_TOP_GAP, RUG_TICK_H,
RUG_BAND_GAP), que o sceneRect e o rótulo de hora inferior já descontam.
O que ela não é: não é histograma (não agrega por faixa, é um traço por viagem), não
é clicável nem tem tooltip, e não distingue linha — só sentido. É derivada, não
editável: sai dos mesmos start_time_s que desenham as barras, então acompanha qualquer
deslocamento feito por >/</+/- assim que a cena é redesenhada. Por ser item de
cena, aparece no PNG/SVG exportado.
6. Interface (block_diagram_dialog.py)¶
QWidget (janela) montado em Python — sem .ui. Layout em QSplitter:
controles | diagrama | detalhes.
- Controles: lista multi-seleção de linhas; serviço (dia); sentido (ida/volta);
janela de tempo em horas (0–30 h, via
QSpinBox—QTimeEditnão passa de 23:59 e o GTFS tem serviço pós-meia-noite); rádios Viagens/Blocos; parâmetros de bloco (layover mín/máx, deadhead, relaxado) visíveis só no Modo Blocos. - Diagrama: a
BlockView/BlockScene. - Detalhes: ao clicar numa viagem, mostra linha, sentido, bloco/veículo, início/fim/duração, nº de paradas, terminais, headsign e IDs.
- Status: nº de viagens e faixas (Viagens) ou nº de veículos (Blocos), além de avisos.
6.1 Integração com o plugin¶
O botão é adicionado por código ao QGridLayout do diálogo principal (sem editar o
.ui). diagramaClicked() resolve o GeoPackage via _resolve_gpkg(), abre a janela
parenteada à janela principal do QGIS (para sobreviver ao fechamento do SIG-Bus) e
fecha o diálogo do SIG-Bus. O import dos módulos do diagrama é tardio: um problema
neles não impede o resto do plugin de carregar.
7. Fluxo de uso¶
- Carregar o GTFS no plugin (gera o
feed.gpkg) — ou “Reconectar GeoPackage”. - SIG-Bus → Diagrama de Blocos (o SIG-Bus fecha).
- Selecionar a(s) linha(s); opcionalmente serviço, sentido e janela.
- Escolher Viagens ou Blocos (neste, ajustar layover/deadhead/relaxado).
- Gerar diagrama. Clicar nas barras para detalhes; no Modo Blocos, clicar mostra o headway. Roda = zoom; botão do meio = pan; Exportar = PNG/SVG.
Para o nº de frota ser real, selecione um único serviço — com “Todos os serviços” a contagem é somada entre dias e há aviso.
8. Limitações e cuidados¶
- Modo Blocos é estimativa (sem
block_id); sensível aos parâmetros de layover e ao casamento de terminais. Em feeds onde os terminais não casam exatamente, usar deadhead ou relaxado. - Deadhead simplificado — não calcula tempo real de reposicionamento entre terminais.
- GTFS estático — reflete o plano da agência, não a operação real (atrasos, supressões). Integração com GTFS-Realtime fica como horizonte futuro.
- Headway aqui é o intervalo programado entre partidas da mesma linha/sentido; não considera paradas compartilhadas por várias linhas.
9. Testes¶
A camada de dados e o algoritmo (puro Python/SQLite) foram validados fora do QGIS com
stubs dos módulos qgis:
ScheduleReader/parse_gtfs_time: 16 verificações (multi-linha, primeira/última parada por sequência, horas ≥ 24 h, filtros de sentido/janela/serviço).BlockBuilder: 12 verificações (casamento de terminal, deadhead, layover mín/máx, relaxado, frota = pico de simultâneas, separação por serviço, encadeamento entre linhas, ociosidade).- Lógica de headway (
_prev_trip): anterior correta por linha+sentido, sem headway na primeira viagem. - Régua de saídas (
test_block_scene_rug.py):departure_ticks(um traço por viagem, banda por sentido,direction_idvazio caindo em'ida', saída ordenada) e a cena (ida estritamente acima de volta,sceneRectcrescendoRUG_H, rótulovoltaausente num diagrama só de ida). - Ajuste de horários e matriz (
test_schedule_edit_core.py,test_schedule_table_core.py,test_block_view_zoom.py,test_gtfs_edit_stop_times.py): deslocamento de viagens e faixas horárias em memória, matriz paradas × viagens, preservação do enquadramento entre redesenhos e gravação destop_timesno GeoPackage.
A camada Qt/GUI (cena, view, diálogo) é validada manualmente dentro do QGIS.