Pular para conteúdo

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.md e ARQUITETURA_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 QPainter pelo 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 de Trip; propriedades span e idle_seconds.
  • Schedule — objeto que a Engine renderiza: trips, blocks, fleet_size, mode, warnings; time_bounds calcula 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.txt tem ~136 MB → a leitura é restrita às linhas selecionadas (route_id IN (...)) e usa os índices idx_st_trip criados por create_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_id pode ser 1→N (a mesma linha tem vários route_id); o IN (...) 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

  • TimeAxisMapper converte 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 - emitem nudgeKeyPressed(str) com o caractere. A leitura é por event.text() (o layout já resolveu a tecla — > e < ficam em teclas diferentes em ABNT2 e US-International), com Key_Plus/Key_Minus do 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" chamam fit_all(), os demais preservam, e o botão Enquadrar tudo reenquadra sob demanda.
  • Export PNG (QImage, 2×) e SVG (QSvgGenerator, se QtSvg presente).

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 — QTimeEdit nã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

  1. Carregar o GTFS no plugin (gera o feed.gpkg) — ou “Reconectar GeoPackage”.
  2. SIG-Bus → Diagrama de Blocos (o SIG-Bus fecha).
  3. Selecionar a(s) linha(s); opcionalmente serviço, sentido e janela.
  4. Escolher Viagens ou Blocos (neste, ajustar layover/deadhead/relaxado).
  5. 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_id vazio caindo em 'ida', saída ordenada) e a cena (ida estritamente acima de volta, sceneRect crescendo RUG_H, rótulo volta ausente 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 de stop_times no GeoPackage.

A camada Qt/GUI (cena, view, diálogo) é validada manualmente dentro do QGIS.