Histórico de versões — SIG-Bus¶
Plugin QGIS de análise de transporte público (PIBIC DPPG 113/2021). Este arquivo detalha, por versão, o que foi feito e por quê — tanto do ponto de vista de transporte público quanto de código. Serve de base para os posts do blog do projeto.
O plugin carrega um feed GTFS num GeoPackage, importa dados de demanda (CSV), aloca embarques nos tramos das linhas, gera relatórios em PDF e desenha o Diagrama de Blocos (alocação de frota). Feed de referência nos testes: o da BHTrans (Belo Horizonte).
0.8.4 — A documentação vira um site, e o Gerenciador de Plugins passa a apontar para ele¶
Esta versão não muda uma linha do que o plugin faz: o feed GTFS, a alocação de embarques nos tramos, o Diagrama de Blocos e os relatórios em PDF saem idênticos aos da 0.8.3. O que muda é onde a documentação se lê. Os oito documentos que hoje estão soltos dentro de sig_bus/ passam a formar um site navegável em https://sigbus.dcamargo.com.br, montado a cada push (decisões 158-164).
Do lado de transporte público¶
- Um endereço para mandar ao analista. O
homepage=dometadata.txtdeixa de apontar para o repositório de código e passa a apontar para o site (decisão 164) — é esse link que o Gerenciador de Complementos do QGIS mostra. Quem recebe a indicação do plugin cai numa página que explica o que ele faz, não num diretório de arquivos.py. É a única mudança que o usuário do QGIS percebe nesta versão. - Uma porta de entrada e uma página de instalação.
docs/index.mdresume em dois parágrafos o que o SIG-Bus junta — o GTFS da operação e a demanda de embarque por parada — e leva, por cartões, a instalação, guias, referência e arquitetura.docs/instalacao.mdreúne o que estava espalhado no README: a faixa de QGIS suportada, o diretório de plugins em Linux e Windows, a ativação em Complementos → Gerenciar e Instalar, e a configuração opcional da geocodificação — inclusive o aviso de que o Nominatim e o Photon impõem 1 s por requisição por host, então geocodificar uma linha inteira leva minutos por política do serviço, não por lentidão do plugin. - Busca em português e leitura no celular, que os arquivos
.mdsoltos no GitHub não davam: o tema Material traz índice lateral, busca com stemmer PT-BR e modo claro/escuro. - O README continua sendo o resumo e agora aponta para o site nas duas metades, EN e PT-BR, mantendo a simetria de cabeçalhos que o
test_readme.pycobra.
Do lado de código¶
- A documentação continua morando em
sig_bus/; o site a copia no build (decisão 159). Esses.mdsão lidos offline por quem instalou o plugin —sig_bus/é a pasta que se copia para o QGIS — e são linkados pelo README, com link relativo guardado portest_readme.py. Movê-los quebraria as duas coisas. Em vez disso,scripts/build_docs_site.pycopia os nove arquivos canônicos para dentro dedocs/segundo um mapa explícito (guias/,referencia/,arquitetura/,changelog.md) e reescreve todo link relativo: alvo.mdconhecido vira caminho relativo à página de destino, alvo que não é página (modelo_paradas.csv,docs/gtfsfiles.zip,docs/PyQGIS_PIBIC.pdf) vira URL do GitHub, e alvo desconhecido levanta erro em vez de gerar link morto. Mesmo espírito dogtfs_schema.py: um lugar só descreve cada coisa, o resto deriva.sig_bus/GUIA_EDICAO_GTFS_RASCUNHO.mdfica de fora — é rascunho superado pelo guia final. - O site é PT-BR, e o inglês entra por uma página só (decisão 162). O gerador parte o
README.mdno divisor# SIG-Bus — Plugin QGISe produzvisao-geral.md(metade PT-BR) een/overview.md(metade EN, ponto de entrada em inglês). Montar tradução do site inteiro criaria uma obrigação que ninguém cumpriria; os dois documentos com conteúdo em inglês (DOCUMENTACAO.mdeMETHODS.md) já são alcançáveis dali. - As páginas derivadas não vão para o git (decisão 160). Um segundo commit da mesma prosa dentro de
docs/apodrece no dia em que alguém edita só um dos dois. O.gitignorebloqueiadocs/guias/,docs/referencia/,docs/arquitetura/,docs/changelog.md,docs/visao-geral.md,docs/en/esite/; quem quiser rodar o site local roda o gerador antes, que é o que os alvosdocs-build/docs-servedosig_bus/Makefilefazem. - MkDocs + Material, não Jekyll (decisão 158). O repositório é 100% Python; introduzir
bundlersó para o site criaria uma segunda cadeia de ferramentas para manter.requirements-docs.txtfixamkdocs,mkdocs-materialepymdown-extensionsem faixas~=e diz no topo que é dependência só do site — o plugin continua rodando com o Python embutido do QGIS, sem dependência nenhuma. docs/vira odocs_dirdo MkDocs, sem mover os binários (decisão 161).docs/PyQGIS_PIBIC.pdfedocs/gtfsfiles.zipjá eram rastreados e já eram linkados pelo README; ficam onde estão e passam a ser baixáveis direto do site. Comodocs/topicos/é pasta de trabalho de agente (ignorada pelo git, presente no disco), omkdocs.ymltrazexclude_docs: topicos/— senão o build local publica arquivo que o CI não tem.- Deploy por
actions/deploy-pages, sem branchgh-pages(decisão 163). O workflow.github/workflows/docs.ymlroda empushparamaine emworkflow_dispatch: gera as páginas, fazmkdocs build --stricte publica o artefato.docs/CNAMEguarda o domínio — sem ele o Pages perde o domínio próprio na primeira republicação. - Nada disso entra no
.zipdo plugin:mkdocs.yml,requirements-docs.txt,scripts/edocs/ganharamexport-ignoreno.gitattributes, junto dos testes que já saíam.
Testes¶
sig_bus/test_docs_site.pygera o site num diretório temporário — sem precisar do MkDocs instalado — e confere que todas as páginas do mapa saem preenchidas, que nenhum link relativo do site aponta para arquivo inexistente, que o rascunho não foi publicado, que as duas páginas do README ficaram cada uma com a sua língua, e que um alvo de link desconhecido estoura em vez de virar link morto.
Arquivos tocados¶
scripts/build_docs_site.py, sig_bus/test_docs_site.py, mkdocs.yml, requirements-docs.txt, docs/index.md, docs/instalacao.md, docs/CNAME, docs/stylesheets/extra.css, .github/workflows/docs.yml, .gitignore, .gitattributes, sig_bus/Makefile, sig_bus/metadata.txt, README.md, CHANGELOG.md.
0.8.3 — A matriz de horários sobe para cima do diagrama, e a seleção anda junto nos dois¶
Esta versão não muda nada no dado nem no cálculo: o feed GTFS, a alocação de embarques nos tramos, o Diagrama de Blocos e os relatórios em PDF saem exatamente iguais aos da 0.8.2. O que muda é o arranjo do editor único de horários — a matriz deixa de ficar ao lado do diagrama e passa a ficar acima dele — e o fato de a viagem selecionada num painel passar a ficar selecionada também no outro (decisões 155-157).
Do lado de transporte público: a tabela ganha a largura inteira e conversa com o diagrama¶
- Matriz em cima, diagrama embaixo (decisão 155). Lado a lado, os dois painéis disputavam a mesma largura, e é a largura que a matriz precisa: cada viagem é uma coluna, e uma linha com dezenas de partidas ficava espremida num painel de 280 px enquanto o diagrama, que cresce no tempo (eixo horizontal) e não no número de viagens, usava o resto. Empilhados num divisor vertical, a matriz ocupa a largura toda da janela e mostra muito mais colunas de viagem sem rolagem lateral, e o diagrama continua legível na faixa de baixo. A janela "Ajustar Horários" nasce, por isso, mais alta e menos larga (1060×780 no lugar de 1180×620), sempre ajustada à tela de quem abre.
- O divisor agora é arrastado para cima ou para baixo. Puxar para baixo dá mais espaço à tabela; para cima, ao diagrama. Nenhum dos dois colapsa a zero — a matriz nunca fica com menos de 140 px de altura.
- Achar na tabela a viagem que se viu no diagrama (decisão 157). Clicar numa barra do diagrama põe o cursor na coluna daquela viagem na matriz; clicar numa célula da matriz seleciona a viagem correspondente no diagrama. Antes, quem via no diagrama a viagem que estava atrasada tinha de procurar coluna por coluna na matriz pelo
trip_iddo tooltip. A seleção também sobrevive ao redesenho: depois de um>/</+/-ou de uma célula editada, a viagem continua selecionada nos dois painéis, com o mesmo extremo (saída ou chegada) que estava ativo.
Do lado de código¶
ui_geometry.pyganhadivisao_vertical(altura_total, fracao=0.45, min_topo=170, min_base=230)(decisão 156), que delega adivisao_splitterem vez de reimplementar a regra: a mesma garantia de soma exata e de divisão proporcional quando os dois mínimos não cabem vale para o eixo vertical. Continua sem depender de Qt.schedule_editor_widget.pytrocaQSplitter(Qt.Orientation.Horizontal)porQt.Orientation.Vertical, adiciona a matriz antes do painel do diagrama (índice 0 = topo), usasetMinimumHeight(140)no lugar desetMinimumWidth(280)edivisao_vertical(620)no lugar dedivisao_splitter(900).- A sincronização é bidirecional e não entra em laço.
_on_trip_clicked(sinaltripClickeddaBlockScene) e_on_grid_current_cell_changed(sinalcurrentCellChangedda tabela) são guardados por um único sinalizador_syncing, além do_rebuildingque já existia; a coluna 0 (Parada) é ignorada, porque não corresponde a viagem nenhuma._trip_selecionada/_endpoint_selecionadoguardam a seleção antes do redesenho e a restauram depois — e são zerados quando a viagem some do rascunho, em vez de tentar selecionar um item inexistente. O caminho de escrita continua único: nada disso tocastop_times, é só seleção. SigBus_dialog.pyajusta a chamada depreparar_janelada janela de horários para 1060×780. A geometria salva noQSettingsdo QGIS continua na mesma chave e sob a mesma regra da 0.8.2 (só volta a valer se ainda couber na tela).
Testes¶
sig_bus/test_ui_geometry.pycobredivisao_verticalnos três casos do irmão horizontal: divisão folgada, altura exatamente igual à soma dos mínimos e altura apertada que teria de zerar um lado.sig_bus/test_schedule_editor_widget.pyganha regressão da ordem dos painéis (matriz em cima, sem colapsar), da sincronização nos dois sentidos, da coluna 0 ignorada, da ausência de laço entre os dois sinais e da seleção que sobrevive ao redesenho — inclusive quandoset_stop_timesremove a viagem que estava selecionada.
Arquivos tocados¶
schedule_editor_widget.py, ui_geometry.py, SigBus_dialog.py, test_schedule_editor_widget.py, test_ui_geometry.py, metadata.txt, CHANGELOG.md, README.md, GUIA_EDICAO_GTFS.md, DIAGRAMA_BLOCOS.md, ARQUITETURA_CONSTRUIR_GTFS.md.
0.8.2 — A janela "Ajustar horários" cabe na tela do notebook¶
Esta versão não muda nada no dado nem no cálculo: o feed GTFS, a alocação de embarques nos tramos, o Diagrama de Blocos e os relatórios em PDF saem exatamente iguais aos da 0.8.1. O que muda é a ergonomia das janelas grandes do plugin em telas menores e a matriz de horários que sumia sufocada pelo texto de instruções (decisões 148-154).
Do lado de transporte público: a janela nasce do tamanho que cabe¶
- "Ajustar Horários" e "Diagrama de Blocos" não nascem mais maiores que a tela. Antes as duas abriam sempre no mesmo tamanho fixo (1180×620 e 1100×640) e, em notebook de tela pequena, parte da janela ficava fora da área visível e não dava para maximizar. Agora as duas abrem já ajustadas à área útil da tela em que o QGIS está rodando — em monitor grande o tamanho desejado continua igual — e ganham os botões de maximizar/minimizar.
- Só "Ajustar Horários" volta do jeito que foi deixada. Ao fechar essa janela (aplicando ao feed ou cancelando), o tamanho e a posição são lembrados; reabrindo, ela volta assim — mas só se ainda couber na tela atual. Quem fechou num monitor externo grande e reabre no notebook não recebe de volta uma janela cortada: nesse caso ela nasce no tamanho ajustado à tela menor. O "Diagrama de Blocos" não guarda geometria; ele só ganhou o mesmo ajuste de nascer do tamanho da tela.
- A tabela de paradas × viagens volta a aparecer. No editor único de horários (diagrama de blocos à esquerda, matriz à direita, separados por um divisor arrastável), a frase de instruções no topo do painel esquerdo não quebrava linha e forçava uma largura mínima que sufocava a matriz. Agora a frase quebra linha, o divisor tem mínimos razoáveis para os dois lados e proporção inicial de 3:2 — nenhum dos dois painéis colapsa a zero.
Do lado de código¶
- Novo módulo
sig_bus/ui_geometry.py, sem dependência de Qt nas funções de cálculo:ajustar_ao_disponivel(nunca devolve mais que a área útil da tela menos uma margem, nunca devolve valor ≤ 0),divisao_splitter(reparte uma largura total entre dois painéis respeitando mínimos, dividindo proporcionalmente quando os dois não cabem) ecabe_na_tela(testa se um retângulo de janela está contido na área útil). Só duas funções tocam Qt de fato:preparar_janela(clampa o tamanho, liga maximizar/minimizar, centraliza — e em ambiente sem tela resolvível apenas redimensiona, sem levantar exceção) erestaurar_se_couber. SigBus_dialog.pyeblock_diagram_dialog.pytrocam oresize()fixo porpreparar_janela(...).SigBus_dialog.pytambém grava a geometria da janela de horários no sinalfinished(cobre tanto "Aplicar ao feed" quanto "Cancelar"), na chaveSIG-Bus/schedule_dialog/geometrydoQSettingsdo QGIS — mesma política já usada pela configuração de geocodificação: preferência de quem usa mora no perfil do QGIS, nunca no arquivo de projeto e nunca nofeed_edit.gpkg.schedule_editor_widget.pyganhasetWordWrap(True)no rótulo de instruções esetChildrenCollapsible(False)no divisor, com larguras mínimas de 320 px (diagrama) e 280 px (matriz) e divisão inicial calculada pordivisao_splitter(900).
Testes¶
sig_bus/test_ui_geometry.py(novo) cobre as funções puras epreparar_janelasobQApplicationreal.sig_bus/test_schedule_editor_widget.pyganha regressão de quebra de linha, divisor não colapsável e larguras mínimas de cada painel.sig_bus/test_gtfs_edit_stop_times.pyganha um teste de que a janela de horários cabe na tela e grava a geometria noQSettings.
Arquivos tocados¶
ui_geometry.py (novo), SigBus_dialog.py, block_diagram_dialog.py, schedule_editor_widget.py, test_ui_geometry.py (novo), test_schedule_editor_widget.py, test_gtfs_edit_stop_times.py, metadata.txt, CHANGELOG.md, README.md, GUIA_EDICAO_GTFS.md.
0.8.1 — Versão em três algarismos: só a numeração muda¶
Esta versão não muda comportamento nenhum do plugin. Ela existe para alinhar o número de versão do SIG-Bus ao padrão dos demais projetos da VPS, que usam três algarismos (X.Y.Z). Nada foi acrescentado, removido ou corrigido no que o plugin faz.
Do lado de transporte público: nada muda¶
- A leitura do feed GTFS para o GeoPackage, a importação dos dados de demanda por parada, a alocação de embarques nos tramos das linhas, o Diagrama de Blocos e os relatórios em PDF continuam exatamente como na 0.8. Os assistentes Construir GTFS e Edição GTFS, o editor único de horários e o validador/exportador também.
- Para o analista, a única diferença visível é o número que o Gerenciador de Complementos do QGIS mostra:
0.8.1no lugar de0.8. Feed carregado, projeto salvo e.zipexportado com a 0.8 continuam válidos — não há migração a fazer. - A faixa de QGIS declarada fica intocada (
qgisMinimumVersion=3.34,qgisMaximumVersion=4.99,supportsQt6=True): o plugin continua instalável do LTR 3.34 ao fim da série 4.x.
Por que três algarismos, e por que 0.8.1 (decisões 143 e 144)¶
- A divergência era de omissão, não de regra (decisão 143). Nenhum arquivo deste repositório mandava usar dois algarismos — o que havia era a permissão: a guarda de formato em
test_metadata.pyaceitava^\d+\.\d+(\.\d+)?$, com o terceiro algarismo opcional, e o passo 1 do "Ritual de Release" doREADME.md, nas duas metades (EN e PT-BR), ensinava literalmenteversion=X.Y. Com o formato de duas partes escrito no ritual e aceito pelo teste, cada release seguiu o exemplo. Os outros projetos da VPS usam três; o alinhamento é trazer o SIG-Bus para três, não afrouxar os outros. 0.8.1, e não0.8.0(decisão 144). A 0.8 já foi lançada: a seção dela está fechada neste arquivo e o pacote correspondente já foi publicado. Reescrever aquele número como0.8.0renomearia uma versão publicada e desalinharia CHANGELOG,.zipe qualquer instalação já feita. A normalização entra, portanto, como um release de patch novo, cujo conteúdo é a própria mudança de convenção. Há precedente no próprio arquivo: a0.5.1também foi uma versão de três algarismos publicada por cima de uma de dois.
A história publicada fica como foi publicada (decisão 145)¶
- As seções
0.8,0.7,0.6,0.5.1,0.5,v0.4,v0.3ev0.2continuam idênticas ao que saiu — inclusive a inconsistência do prefixo "v" nas mais antigas. A regra dos três algarismos vale daqui para frente; retroagir seria mentir sobre o que foi publicado. Na prática, o diff deste release só acrescenta linhas no topo do arquivo.
Um só ponto de edição, e uma guarda que impede a recaída (decisões 146 e 147)¶
- A convenção passa a ser guarda de teste, não disciplina (decisão 146). O regex de
test_metadata_version_guardaperta para exigir os três algarismos, com teste próprio da regra —0.8.1,1.0.0e0.10.2aceitos;0.8,1e0.8.1.2recusados —, porque sem isso o próximo release voltaria a0.9por hábito, exatamente como a permissão da decisão 143 produziu a série atual. A guarda de não-regressão (parts >= [0, 4]) continua valendo sem ajuste: ela compara tupla de inteiros, e[0, 8, 1] >= [0, 4]. - O número mora num lugar só (decisão 147). Nenhum
.pydo plugin carrega a versão hardcoded: ela vive na linhaversion=desig_bus/metadata.txt, e tanto este CHANGELOG quanto o nome do.zipsão derivados. Esta versão não cria constante__version__nem duplica o número em lugar nenhum. Empacotar o.zipe criar a tagv0.8.1continuam sendo ritual de release, fora deste registro.
Arquivos tocados¶
metadata.txt, test_metadata.py, README.md, CHANGELOG.md.
0.8 — Editor único de horários (diagrama + matriz), retorno automático da janela e guardas na edição¶
Esta versão entrega, de fato, o ScheduleEditorWidget: o ciclo da 0.7 deu a peça como pronta sem que ela existisse no repositório — nenhum arquivo, nenhuma menção (decisão 133). Aqui ela é escrita, e vira o editor de horários único das duas telas: a página "Horários" do assistente Construir GTFS e a janela "Ajustar horários" da aba Edição GTFS passam a compartilhar o mesmo componente, em vez de duas implementações paralelas. Além disso, o fluxo de edição no QGIS fica mais seguro contra descarte acidental de trabalho, e a matriz de horários fica mais legível para quem não decorou o trip_id.
O editor único: diagrama e matriz sobre o mesmo rascunho (decisões 139 e 141)¶
- Um widget, duas telas.
sig_bus/schedule_editor_widget.pymonta numQSplitterhorizontal o diagrama de blocos (BlockView/BlockScene, com oQSpinBoxde passo, o botão "Enquadrar tudo" e um rótulo de status) à esquerda e a matriz paradas × viagens (ScheduleGridWidget) à direita. Os dois lados são vistas do mesmo rascunho destop_timesem memória, e há um caminho de escrita só: os atalhos do diagrama (>/<movem a saída ou a chegada da viagem selecionada;+/-movem a viagem inteira) e a célula editada na matriz desembocam nas mesmasshift_trip/shift_trip_endpointdeschedule_edit_core.py. Depois de cada mudança o diagrama é redesenhado preservando o enquadramento (viewport_state/restore_viewport, decisões 109-111 da 0.7) e a matriz é remontada a partir do rascunho. - As faixas de frequência não migraram (decisão 141). A tabela de faixas horárias, "Adicionar faixa"/"Remover faixa" e "Restaurar frequência regular" continuam só na página "Horários" do assistente — são do assistente, que gera oferta do zero, e não do editor, que ajusta uma oferta que já existe.
O que grava é o diff, nunca a grade inteira (decisões 140 e 118)¶
changed_rows()usa a função puradiff_stop_times(original, atual)deschedule_edit_core.py, que casa as linhas por(trip_id, stop_sequence)e devolve só aquelas em quearrival_timeoudeparture_timemudaram frente aostop_timescomo veio dofeed_edit.gpkg. Linha nova ou ausente é ignorada — esta tela não cria nem apaga viagem. Grade sem edição nenhuma não gera nenhumUPDATE.- Mudança de comportamento na matriz (decisão 112): editar uma célula agora desloca a viagem inteira — o usuário digita o horário que quer naquela parada e o resto da viagem acompanha, preservando os tempos de percurso entre paradas — em vez de gravar aquela célula isolada, como fazia a versão anterior.
Cabeçalho legível da matriz (decisões 135 e 136)¶
- Cada coluna de viagem passa a se chamar
V<n>na primeira linha do cabeçalho e traz a primeira saída da viagem emHH:MMna segunda (ex.:"V1\n06:10"), com otrip_idcompleto só no tooltip — ninguém precisa mais decorar umtrip_idpara achar a viagem certa. - A primeira coluna, "Parada", passa a trazer o nome da parada (
stop_name, caindo nostop_idquando o feed não tem nome) em vez dostop_idcru, com ostop_idno tooltip. O nome vem de umLEFT JOIN stopsacrescentado na própria consulta que monta a matriz.
Guardas contra perda de trabalho no fluxo de edição (decisões 137, 138 e 142)¶
- A cópia de trabalho do assistente nasce sob demanda (decisão 137). Antes, só entrar na aba "Construir GTFS" já criava
feed_edit.gpkg. Agora a criação foi movida para_ensure_build_working_copy(), emSigBus_dialog.py, chamado só no primeiro ponto que de fato grava algo (salvar a agência) e no guarda da página "Paradas" — espiar a aba não cria mais cópia nenhuma. - "Entrar no modo edição" nunca fica desabilitado (decisão 138). Com uma edição ativa, o rótulo de status passa a dizer de qual arquivo a cópia veio (por exemplo, "Edição em andamento: feed_edit.gpkg (cópia de bhtrans.gpkg)") ou avisa que é a cópia vazia criada pelo assistente, e a pergunta ao reentrar explicita o que cada resposta faz: "Sim" recria a partir do GTFS carregado (o que não tiver sido exportado se perde), "Não" retoma a edição atual.
- "Abrir para edição" devolve o plugin sozinho (decisão 142). A janela do SIG-Bus passa a se esconder (
hide(), nãoclose()) em vez de fechar, e volta (show()/raise_()/activateWindow()) quando a tabela de atributos do QGIS fecha (e, se a API do QGIS não devolver esse diálogo, quando a edição da camada termina). Na volta, se a camada ainda tiver alterações não gravadas no buffer de edição, o plugin pergunta se grava (commitChanges()) ou mantém a camada em edição.
Arquivos tocados¶
schedule_editor_widget.py (novo), test_schedule_editor_widget.py (novo), test_edit_tab_guards.py (novo), SigBus_dialog.py, gtfs_edit_core.py, schedule_edit_core.py, schedule_grid_widget.py, schedule_table_core.py, test_gtfs_edit_stop_times.py, test_schedule_edit_core.py, test_schedule_table_core.py, metadata.txt, CHANGELOG.md, GUIA_EDICAO_GTFS.md, ARQUITETURA_CONSTRUIR_GTFS.md, DIAGRAMA_BLOCOS.md.
0.7 — Ajuste de horários: zoom preservado, faixas horárias e edição no feed¶
Versão focada no ajuste de oferta: o que a 0.5 abriu (deslocar viagem no
Diagrama de Blocos com >/</+/-) vira uma tela de trabalho de verdade —
com o enquadramento que não se perde a cada tecla, faixas horárias por período
do dia e o ajuste de horários disponível também para um feed já carregado,
não só para o que o assistente acabou de criar.
Do ponto de vista de transporte público, é a diferença entre desenhar uma
oferta uniforme e desenhar a oferta real: intervalo e duração de viagem
passam a variar por faixa (pico manhã / entrepico / pico tarde), que é o que o
Diagrama de Blocos usa para estimar frota — manter a duração fixa no pico
produz um stop_times que subestima o tempo de ciclo. E o ajuste fino deixa de
ser exclusivo de feed novo: dá para abrir uma linha do feed em edição, mexer
nos horários e gravar de volta.
O zoom não se perde a cada ajuste (decisões 109-111)¶
- A culpa era do redesenho, não do zoom.
_render_schedule_diagram()terminava chamandofit_all()(resetTransform()+fitInView()), então cada tecla de nudge reenquadrava o diagrama inteiro e jogava fora o zoom que o usuário tinha acabado de dar na viagem que estava olhando. O redesenho passa a preservar o enquadramento corrente; enquadrar tudo virou ação explícita, no botão "Enquadrar tudo" ao lado do "Passo". - Preserva transformação e posição, não só a escala (decisão 110):
BlockViewganhouviewport_state()/restore_viewport(), que guardam aQTransforme o centro em coordenadas de cena. Guardar só o fator de escala devolveria o zoom certo no lugar errado, já que a view usaAnchorUnderMousee uma viagem deslocada pode esticar osceneRect. - Primeiro desenho enquadra, os seguintes preservam (decisão 111): a regra
mora no chamador — sem estado anterior (cena vazia, primeira entrada na
página, "Restaurar frequência regular"),
fit_all(); com estado anterior,restore_viewport(). ABlockViewcontinua sem conhecer o modelo.
Faixas horárias no "Construir GTFS" (decisões 119-125)¶
- Faixas substituem o par único de hora início/fim (decisão 119): a página
de horários agora tem uma tabela de faixas (
Início,Fim,Intervalo,Duração), que começa com uma linha preenchida com os valores de sempre (06:00→23:00, 30 min de intervalo, 30 min de duração). Quem não quer desagregar não muda nada no que faz, e uma faixa reproduz exatamente a grade que a 0.6 gerava. - A duração da viagem também é por faixa (decisão 120): no pico o mesmo percurso demora mais, e é essa duração que vira tempo de ciclo no Diagrama de Blocos.
- A UI oferece até 3 faixas; a função pura não impõe limite (decisão 121):
"Adicionar faixa" para no terceiro item — pico manhã / entrepico / pico tarde
—, mas
schedule_edit_core.expand_bands_to_stop_times()aceita N faixas, emdictou tupla. - Fronteira de faixa não duplica saída (decisão 122): com faixas
06:00–09:00e09:00–16:00, a saída das 09:00 seria gerada duas vezes — fim inclusivo de uma, início da outra —, criando duas viagens no mesmo horário. A expansão percorre as faixas em ordem cronológica e descarta a saída já gerada; a faixa mais cedo é quem vence. - Faixas sobrepostas são erro, não aviso (decisão 123):
schedule_edit_core.validate_bands()reprova sobreposição,fim < início, intervalo ≤ 0 e duração ≤ 0 antes de expandir, com a mensagem nomeando a faixa ("faixa 2 (09:00–16:00) sobrepõe a faixa 1"). Buraco entre faixas é legítimo — linha que não opera no entrepico — e passa sem reclamar. save_routeaceita as três formas defrequencia(decisão 124): lista de faixas (nova),dicte tupla (as duas já suportadas) continuam funcionando; o caminho normal do assistente nem passa por lá, porque mandastop_timesjá ajustado._draft_signaturepassa a enxergar as faixas (decisão 125): é essa assinatura que decide se a grade em memória é regerada ou preservada — sem as faixas, mexer numa faixa não regeraria nada e a tela mostraria a oferta antiga. O resumo da página soma as viagens de todas as faixas e mostra a amplitude do intervalo (ex.: "34 viagens · intervalo de 10 a 30 min").
Ajustar horários de um feed já carregado (decisões 117-118)¶
- Botão "Ajustar horários" na aba "Edição GTFS" (habilitado só com edição
ativa e uma linha escolhida), que abre a matriz de horários daquela
linha: uma aba por sentido, paradas nas linhas, viagens nas colunas, os
horários digitados direto na célula. A matriz é montada pelo núcleo puro
schedule_table_core.py(build_schedule_table), sem Qt. - Leitura sempre filtrada por linha (decisão 117):
gtfs_edit_core.load_route_stop_times(gpkg, route_short_name, service_id=None)vai deroute_short_name(+service_idopcional) →trips→stop_timesdaquelas viagens, e nada mais. A decisão 5 (nunca carregarstop_timesinteiro) vale igual aqui: num feed real como o da BHTrans essa tabela tem milhões de linhas. - Gravação é
UPDATEpor (trip_id,stop_sequence), em transação (decisão 118):gtfs_edit_core.apply_stop_times()altera sóarrival_time/departure_time, e só das células realmente editadas — o tempo parado de cada parada (departure - arrival) anda junto com a saída, em vez de ser achatado. Nenhuma linha é apagada e nenhum id é reescrito — o feed é de terceiros e as viagens carregamshape_id,block_ide o que mais o feed trouxer —, e erro no meio fazrollback. Antes de gravar, a grade ajustada passa pelo mesmovalidate_draft_timesdo assistente: erro bloqueia, aviso pergunta; "Cancelar" não toca no arquivo. - O validador aprendeu horário fora de ordem:
GtfsValidatorpassa a apontar, por SQL agregado, a viagem cuja chegada numa parada é anterior à partida da parada anterior — a falha que um ajuste de horário pode introduzir e que nenhuma das checagens de formato pegava (decisão 6: um validador só).
Ainda não entregue nesta versão¶
O painel lateral de horários por sentido ao lado do diagrama (decisões 112-115) e o widget único de edição compartilhado entre o assistente e a aba "Edição GTFS" (decisão 116) não entraram na 0.7: o ajuste no assistente continua sendo pelo diagrama e pelos atalhos, e na aba de edição é pela matriz descrita acima. Ficam para a versão seguinte.
Versão e documentação (decisões 103, 126)¶
A versão sobe para 0.7 — a fase acrescenta funcionalidade nas duas abas, não
é correção (mesmo critério das decisões 85 e 92). O .zip continua fora do
plano (decisão 94): empacotar segue sendo ritual manual. README.md (nas duas
metades, EN e PT-BR, no mesmo passo — decisão 103), DIAGRAMA_BLOCOS.md,
GUIA_CONSTRUIR_GTFS.md e GUIA_EDICAO_GTFS.md foram atualizados junto.
Arquivos tocados¶
schedule_table_core.py (novo), schedule_grid_widget.py (novo),
schedule_edit_core.py,
gtfs_edit_core.py, gtfs_validator.py, gtfs_builder_core.py,
block_view.py, SigBus_dialog.py, metadata.txt, README.md,
DIAGRAMA_BLOCOS.md, GUIA_CONSTRUIR_GTFS.md, GUIA_EDICAO_GTFS.md,
CHANGELOG.md, test_block_view_zoom.py (novo),
test_schedule_table_core.py (novo), test_gtfs_edit_stop_times.py (novo),
test_schedule_edit_core.py, test_gtfs_builder_progress.py,
test_block_scene_headway.py.
0.6 — Leitura do Diagrama de Blocos: cota enxuta e régua de saídas¶
Duas mudanças de leitura no Diagrama de Blocos, pedidas depois de usar o ajuste fino de horários da 0.5. Nenhuma delas muda dado: as duas só mudam o que o diagrama conta a quem olha.
Do ponto de vista de transporte público, a régua de saídas é a leitura que o quadro de horários não dá de graça: quantas partidas por faixa horária, separadas por sentido — exatamente o que se olha para decidir se o pico está coberto e se o intervalo entre-pico está frouxo.
- A cota mostra só a medida (decisão 96): o rótulo do indicador de headway
era
headway 12 min; agora é12 min. Numa cota de desenho técnico o que se lê é a medida — o que ela mede já está dito pela geometria (duas chamadas verticais partindo de dois inícios da mesma linha e sentido). De quebra, o rótulo curto cabe entre duas viagens próximas sem invadir a barra vizinha. - Régua de saídas na base do diagrama (decisões 97-101): um traço vertical
curto e discreto por partida, ida na banda de cima e volta na de baixo, no
pé do eixo de tempo. A mancha de traços mostra pico e vale por sentido sem
precisar varrer o diagrama faixa por faixa. É derivada dos mesmos
start_time_sque desenham as barras (departure_ticks), então nasce correta e acompanha qualquer deslocamento feito por>/</+/-. Não é histograma, não é clicável e não distingue linha; por ser item de cena, sai no PNG/SVG exportado.
0.5.1 — Faixa de versões do QGIS: o plugin recusado no 4.2 e no 3.34¶
Correção de empacotamento, não de lógica: nas duas pontas da faixa declarada, o gerenciador de complementos recusava um plugin que o código já suportava.
- O teto barrava o QGIS 4.2 (decisão 82): a 0.5 prometeu
qgisMaximumVersion=4.99e gravou3.99. O gerenciador mostrava "Plugin designed for QGIS 3.40 - 3.99" e marcava o plugin como incompatível em todo QGIS 4.x — apesar de a compatibilidade Qt6 já estar feita e testada. Vale lembrar por que a chave não pode simplesmente sumir: ausente, o QGIS assume<major do mínimo>.99, ou seja o mesmo 3.99 que causou o problema. - O piso excluía o QGIS 3.34 (decisões 87-89):
qgisMinimumVersion=3.40não descrevia requisito nenhum do plugin — existia por causa de uma linha,FIELD_STRING = QMetaType.Type.QString, já queQgsField(nome, QMetaType.Type)só existe a partir do 3.38. A sondagem contra o 3.34.4 real mostrou que todo o resto (enums qualificados,writeAsVectorFormatV3, roteamento, import dos módulos) já roda lá. O tipo de campo agora é resolvido por capacidade (_resolve_field_types, com fallback paraQVariant), e o piso desce para o LTR 3.34 que o Ubuntu 24.04 empacota. - Faixa final: 3.34 – 4.99, com guarda em
test_metadata.py(comparando por tupla de inteiros — em ordem lexicográfica'3.99' > '4.99') e sondagem manual contra o QGIS instalado viasig_bus/scripts/check_qgis_compat.py. - O CHANGELOG não fica mais aberto (decisão 93): um teste falha se sobrar
seção "Não lançado" ou se a primeira seção de versão não casar com o
version=dometadata.txt— foi assim que as Fases 7 a 12 ficaram publicadas sob "Não lançado" com a 0.5 já no ar.
0.5 — Construir GTFS, geocodificação e ajuste fino de horários¶
(Seções abaixo: entraram todas na 0.5, publicada sem que o CHANGELOG fosse fechado — daí a guarda da 0.5.1.)
Ajuste fino dos horários no Diagrama de Blocos (Fase 12)¶
A página "Horários" do assistente "Construir GTFS" pedia um único intervalo e o propagava igual para o dia inteiro. Na operação real o intervalo encurta no pico e alarga fora dele — esta fase permite acertar viagem a viagem ainda no bloco de construção, antes de a linha virar dado gravado, reaproveitando o Diagrama de Blocos que o plugin já tem.
Do ponto de vista de transporte público, é a diferença entre um quadro de horários teórico (frequência constante das 5h às 23h) e o quadro que a operação realmente pratica. E o ajuste é feito onde ele custa menos: antes da gravação, sem precisar abrir a tabela crua de stop_times depois.
- Duração da viagem (decisão 81a):
expand_frequency_to_stop_timesganhouduracao_min. Antes, todas as paradas de uma viagem recebiam o mesmo horário — viagem de duração zero, sem chegada para deslocar e comarrival_time == departure_timena última parada. Agora os horários são distribuídos linearmente entre a primeira e a última parada. Sem o parâmetro, o comportamento antigo é preservado. trip_idúnico por linha e sentido (decisão 81b): o id gerado eratrip_<HHMMSS>, sem linha nem sentido — duas linhas que saem 06:00 produziam o mesmotrip_id, e o editor indexa viagem portrip_id. O parâmetroprefixcompõetrip_<linha>_<sentido>_<HHMMSS>.- Núcleo puro
schedule_edit_core.py(decisão 74): deslocar viagem (shift_trip), deslocar extremo com re-interpolação do miolo (shift_trip_endpoint), resumir a grade (trips_from_stop_times), calcular intervalos (headways), validar (validate_draft_times→(erros, avisos)) e montar oScheduleda cena (schedule_from_draft) — tudo sobre listas de dicionários, sem Qt, coberto portest_schedule_edit_core.py. - Atalhos de teclado (decisões 76-78):
>/<movem só a saída ou só a chegada (o clique na metade esquerda/direita da barra escolhe qual) e redistribuem as paradas intermediárias;+/-movem a viagem inteira preservando a duração. As teclas são lidas porevent.text()—>e<ficam em teclas diferentes em ABNT2 e US-International. O passo é configurável (padrão 15 min). - Headway vira cota de desenho técnico (decisão 75): era uma diagonal entre os centros de duas barras em sub-linhas diferentes; agora é uma linha horizontal com linhas de chamada verticais até os dois inícios e o valor no meio — e passa a valer também no Modo Viagens, que é o modo da página de ajuste.
- Gravar não apaga mais o ajuste (decisão 80):
save_routeganhoustop_times=None. Com a grade ajustada, grava exatamente aquelas linhas em vez de reexpandir a frequência; sem o parâmetro, nada muda para quem já chamava a função. - Um ajuste vale para todos os dias do calendário (decisão 72): no GTFS um único conjunto de viagens já atende os cinco dias úteis — quem diz "seg a sex" é o
calendar. O comportamento já era esse; o que faltava era a tela dizer isso, e agora um rótulo acima do diagrama lista os dias.
Google Maps opcional + Overpass como último degrau grátis (Fase 11)¶
Esta fase adiciona o suporte opcional à Google Geocoding API como primeiro provedor de geocodificação e introduz o corretor de grafia via Overpass (OSM) como último recurso para resolver nomes de vias digitados incorretamente.
Do ponto de vista de transporte público, a integração com o Google resolve casos de endereços recém-criados, estabelecimentos ou locais comerciais que ainda não figuram nas bases públicas do OpenStreetMap, garantindo que o assistente de construção de GTFS encontre o ponto com alta precisão sem depender exclusivamente de coordenadas manuais.
- Google Geocoding API opcional (Decisões 62, 63, 66): Quando uma chave de API é fornecida e o modo está em
auto, oGoogleGeocoderé acionado primeiro na cascataGoogle → Nominatim → Photon. A chave é armazenada com segurança noQSettingsdo usuário e não é salva no GeoPackage do projeto (Decisão 62). Candidatos em nível genérico de localidade/município são automaticamente filtrados para evitar posicionamento incorreto no centro da cidade (Decisão 66). - Corretor de vias Overpass (
street_index.py, Decisão 68): Se todos os provedores retornarem vazio e houver contexto de município, o módulo realiza o levantamento das vias reais da região via Overpass e utiliza busca por similaridade textual (difflib,cutoff=0.80) para corrigir o logradouro e refazer uma consulta ao Nominatim com o nome corrigido — que resolve o número da casa; se ela também falhar, o ponto sai docenterda via, sempre com o(via: <nome real>)da decisão 59 declarando o palpite. - Redação de credenciais em logs (
_redigir_credenciais, Decisão 65): Parâmetros sensíveis (key=,api_key=,token=e afins) são ocultados com***nas mensagens gravadas no log do QGIS, prevenindo o vazamento de chaves privadas em relatórios de suporte. - Identificação da procedência do ponto (Decisão 70): Rótulos de status na UI informam a origem exata do resultado (
✓ localizado (Google),✓ localizado (Nominatim),✓ localizado (Photon)ou✓ localizado (via: ... — OSM)).
O Nominatim não perdoa erro de digitação (Fase 10)¶
Depois das correções da Fase 9 a requisição saía e o log provava isso — e mesmo
assim o botão Geocodificar continuava devolvendo "não encontrado". A medição
de 2026-08-05, reproduzindo as URLs do log contra a API pública uma variável por
vez, isolou a causa: viewbox, bounded=1, número da casa e acentuação são
todos indiferentes; a única variável que zera o resultado é a grafia do
logradouro. Rua Giusepe Fórmolo (um p a menos que o Giuseppe real do
OpenStreetMap) devolve 0 candidatos, na busca estruturada e na livre. A
causa deixou de ser técnica e passou a ser de dado — e as seis tentativas da
cascata falhavam juntas porque eram o mesmo motor consultado seis vezes.
Do ponto de vista de transporte público, isso é o caso comum: o itinerário
vem de planilha ou de papel da operadora, digitado por gente, e um nome de rua
de origem italiana (Giuseppe Fôrmolo, na serra gaúcha) erra fácil. Sem
tolerância a typo, o cadastro de paradas por endereço simplesmente não sai do
lugar.
- Photon como último degrau da cascata (Decisão 57): o
photon.komoot.ioé o geocodificador do Komoot sobre os mesmos dados do OSM, público, sem chave e tolerante a erro de digitação por construção. Verificado:q=Rua Giusepe Fórmolo+ bbox de Caxias do Sul trazRua Giuseppe Fôrmoloem 1º e 2º lugar. O Nominatim não foi substituído — continua sendo quem faz busca estruturada e resolve número de casa; o Photon só é consultado depois de a cascata inteira ter voltado vazia. Uma requisição a mais apenas no caso que hoje falha, zero custo no caminho feliz. Dois detalhes medidos viraram código:lang=ptdevolve HTTP 400 (não enviarlang), e abboxdo Photon éminLon,minLat,maxLon,maxLat— ordem diferente doviewboxdo Nominatim já gravado embuild_city_viewbox. - Transporte separado de interpretação (
geocoding.py):_get_json(url)faz a requisição, respeita o intervalo de 1 s e devolve o JSON decodificado (listoudict);_buscarvirou uma casca fina sobre ele para o formato do Nominatim, e oPhotonGeocodertem a sua, para o GeoJSON do Photon — que é normalizado no mesmo dicionáriolat/lon/display_nameque a UI já consome. - Correção de grafia nunca é silenciosa (Decisão 59): quando o logradouro do
candidato aceito difere do digitado, o status da parada vira
✓ localizado (via: <nome real>)em vez de✓ localizado, e o par vai para o logSIG-Bus. A mesma resposta do Photon trouxeRua Giusepe Bressan, uma rua diferente e existente no mesmo município — o acerto não é garantido, e o assistente não corrige o cadastro do usuário pelas costas dele. A comparação normalizada (minúsculas, acentos removidos, espaços colapsados) mora emaddress_format.normalizar_logradouro, que já é a fonte única do padrão de endereço. - A mensagem de "nada localizado" parou de culpar o município (Decisão 60): antes ela mandava "Confira o município na página da agência"; no caso relatado o município estava certo e a orientação levou o usuário a procurar no lugar errado. Agora lista até 3 dos endereços que falharam, aponta a grafia do logradouro como causa mais provável e lembra de "Marcar no mapa" como saída.
- Cache de sessão e menos trabalho repetido (Decisão 61): com o degrau novo o pior caso virou 7 requisições de 1 s por parada, e uma linha importada por CSV tem dezenas delas. Passou a haver cache de sessão por URL, e "Geocodificar" pula as paradas que já têm coordenada — o que também impede que um ponto marcado à mão no canvas seja sobrescrito por um clique a mais.
- Cada tentativa etiquetada no log:
a-estruturada-num,b-estruturada,c-livre,sem-bbox …,photon,city-bbox, junto deerro=ecandidatos=, para o próximo diagnóstico não exigir reconstruir a URL à mão. - Alternativa recusada, registrada para ninguém refazer (Decisão 58): índice
de vias por Overpass +
difflibtambém funciona (4.342 vias de Caxias do Sul em 3,8 s; ratio 0,923 no 1º lugar), mas resolve só o nome da via — ainda exigiria voltar ao Nominatim pela coordenada — e pede cache por município e um limiar a calibrar. Fica como plano B se o Photon público sair do ar.
Geocodificação no QGIS 4 (Fase 9): enum de rede e bbox por município¶
No QGIS 4 (Qt 6) o botão Geocodificar devolvia "não encontrado" para todo
endereço, com bairro e sem bairro. A causa era um enum não qualificado —
QNetworkReply.NoError, que o PyQt6 removeu — levantando AttributeError dentro
do try do NominatimGeocoder._buscar e sendo engolido pelo except Exception:
return []. Toda requisição voltava vazia, sem nenhuma mensagem.
- Enum de rede corrigido (Decisão 51):
QNetworkReply.NetworkError.NoErrore, na mesma varredura,QgsVectorFileWriter.WriterError.NoError/ActionOnExistingFile.CreateOrOverwrite*(SigBus_dialog.py,gtfs_reader.py),QgsBlockingNetworkRequest.ErrorCode.NoErroreQgsVectorLayerDirector.Direction.DirectionBoth(osm_routing.py) eQgsLayoutExporter.ExportResult.Success. Todas as formas foram verificadas no QGIS 3.44/Qt5 e no QGIS 4.2/Qt6 — um codebase só, sem shim de versão. - Geocodificação deixou de falhar em silêncio (Decisão 52): cada tentativa
registra no painel Log Messages, aba
SIG-Bus, a URL consultada, o código de erro e o número de candidatos; exceção vai comtracebackcompleto. Erro de programação deixou de ser indistinguível de "endereço inexistente". bounded=1virou filtro de qualidade, não regra dura (Decisão 53): se toda a cascata restrita à caixa envolvente do município voltar vazia, ela é repetida semviewbox/boundedantes de declarar "não encontrado" — bbox errada ou de município homônimo não zera mais o resultado. Na busca livre, o bairro não é mais repetido quando é o próprio município.- Caixa envolvente do município recalculada na UI (Decisão 54): salvar a agência
grava
build_city_viewboxjunto de município/UF e a invalida quando o par muda — a bbox nunca fica cacheada apontando para outra cidade. Falha de rede aí não bloqueia o salvamento. - Guarda de Qt6 ampliada (Decisão 55):
test_qt6_compat.pypassou a cobrirQNetworkReply,QgsVectorFileWriter,QgsBlockingNetworkRequest,QgsVectorLayerDirectoreQgsLayoutExporter, e a varrer também os arquivos de teste — os mocks doconftest.pyexpunham a forma curta e escondiam a regressão. - Mensagem de resumo com contexto: quando nenhuma parada é localizada, o aviso
mostra o município/UF usados na busca e aponta o log
SIG-Bus.
Arquivos tocados¶
geocoding.py, SigBus_dialog.py, gtfs_reader.py, osm_routing.py,
conftest.py, test_geocoding.py, test_qt6_compat.py,
GUIA_CONSTRUIR_GTFS.md, ARQUITETURA_CONSTRUIR_GTFS.md, CHANGELOG.md.
Construção de GTFS (Fase 8): Padrão de Endereço, Geocodificação e Lote¶
Melhorias de legibilidade, usabilidade e robustez na aba Construir GTFS:
- Legibilidade e Temas (Decisão 42): padronização das folhas de estilo em constantes centralizadas em
SigBus_dialog.py(QSS_INPUT,QSS_CARD,QSS_HINT,QSS_STATUS_OK,QSS_STATUS_ERR), garantindo contraste legível em temas claros e escuros (Night Mapping). - Padrão de Endereço (Decisão 43): formato padronizado
Logradouro, Número - Bairroviasig_bus/address_format.py, com Município e UF configurados globalmente na agência. - Geocodificação Estruturada por Contexto (Decisões 44, 45 e 47): busca síncrona no Nominatim em cascata (com número, sem número, e busca livre), restrita ao contexto e à caixa envolvente (bounding box /
viewbox) do Município configurado. - Tabela Interna
sig_bus_config(Decisão 46): persistência de metadados da agência e caixa envolvente do município no GeoPackage de trabalho, ignorada na exportação GTFS. - Indicadores Visuais de Status e Supressão de Lat/Lon (Decisão 48): remoção dos campos visuais de latitude e longitude da tabela de paradas, substituídos por status visuais (
✓ localizado,✗ não encontrado,📍 marcado no mapa). - Marcação no Mapa (Decisões 49 e 50): botão Marcar no mapa ativa a ferramenta interativa
PickStopPointTool(map_tools.py) para selecionar coordenadas com um clique no canvas (ideal para linhas rurais), com adição automática do raster OpenStreetMap (ensure_osm_basemap). - Importação de Paradas em Lote via CSV (Decisão 43): suporte à importação por arquivo CSV (delimitador
;, UTF-8 com BOM,stops_csv.py), com o modelo de exemplomodelo_paradas.csve guia explicativoMODELO_PARADAS_CSV.md.
Arquivos tocados¶
SigBus_dialog.py, geocoding.py, gtfs_builder_core.py, address_format.py (novo), stops_csv.py (novo), map_tools.py (novo), modelo_paradas.csv (novo), MODELO_PARADAS_CSV.md (novo), GUIA_CONSTRUIR_GTFS.md, ARQUITETURA_CONSTRUIR_GTFS.md, README.md, CHANGELOG.md.
Suporte a QGIS 4 / Qt 6¶
O QGIS 4 roda sobre Qt 6, e o PyQt6 removeu os enums "curtos" do Qt 5
(Qt.AlignTop, Qt.Horizontal, QMessageBox.Yes, .exec_() etc.), assim
como o QGIS 4 removeu os aliases depreciados dos seus próprios enums
(Qgis.Critical, QgsTask.CanCancel, QgsUnitTypes.LayoutMillimeters). Sem
essa migração o plugin nem abria no QGIS 4 (AttributeError: type object 'Qt'
has no attribute 'AlignTop'). A forma qualificada usada aqui vale nos dois
ambientes: um único caminho de código, sem shim de versão, rodando tanto
no QGIS 3.40 LTR (Qt 5) quanto no QGIS 4 (Qt 6).
- Enums qualificados em todo o pacote:
SigBus.py,SigBus_dialog.py,gtfs_reader.py,gtfs_export.py,block_core.py,block_diagram_dialog.py,block_scene.pyeblock_view.pypassaram a usar a forma qualificada dos enums Qt/QGIS (Qt.AlignmentFlag.AlignTop,Qgis.MessageLevel.Critical,QgsTask.Flag.CanCancel,Qgis.LayoutUnit.Millimeters,.exec()). QVariant.Type→QMetaType.Type:QVariant.Typenão existe mais no PyQt6, entãoQgsField(nome, QVariant.String)é quebra dura no QGIS 4.gtfs_reader.pyexpõeFIELD_STRING/FIELD_INT(QMetaType.Type.QString/.Int), usados por todas as criações de campo do plugin.qgisMinimumVersionatualizado de3.0para3.40esupportsQt6=Trueacrescentado emmetadata.txt— oQgsFieldcomQMetaTypeexige QGIS ≥ 3.38 (3.40 é o LTR), e sem o flag o QGIS 4 não considera o plugin instalável.- Guarda de regressão: novo
test_qt6_compat.pyvarre todo o pacote por padrão de texto em busca de enums na forma antiga (Qt5) e falha se algum reaparecer — evita que copy-paste de código antigo reintroduza a regressão.
Arquivos tocados¶
SigBus.py, SigBus_dialog.py, gtfs_reader.py, gtfs_export.py,
block_core.py, block_diagram_dialog.py, block_scene.py,
block_view.py, metadata.txt, conftest.py, README.md,
test_qt6_compat.py (novo).
v0.4 — Refino do Diagrama de Blocos e reorganização da interface¶
Versão focada em legibilidade do diagrama e em fidelidade do modelo de frota.
Interface em abas¶
A janela principal do plugin foi reorganizada em duas abas, seguindo o fluxo de trabalho:
- Entrada de dados — carregar o GTFS (.zip), carregar a demanda (.csv) e Reconectar GeoPackage (que é, por definição, uma operação de fonte de dados).
- Análise — escolher a linha, Filtrar dados, Alocar Demanda, Gerar Relatório e Diagrama de Blocos.
A barra de Ajuda + OK/Cancelar ficou comum às duas abas. O botão do diagrama,
antes criado em tempo de execução por código, passou a viver no próprio .ui.
Eixo de tempo mais legível¶
No diagrama, o eixo de tempo ganhou:
- Linhas tracejadas nas meias-horas (12:30, 13:30…), subordinadas visualmente às linhas cheias das horas — ajudam a situar viagens no meio da hora.
- Rótulos de hora em cima e embaixo, para referência nas duas pontas do diagrama (útil quando ele fica alto, com muitas faixas).
Terminais: nome e sigla¶
O feed da BHTrans não tem código/sigla de terminal, mas o trip_headsign
(destino da viagem) está sempre preenchido (184 destinos distintos). A partir dele:
- O tooltip e o painel de detalhes mostram o terminal de destino legível
(ex.:
ESTACAO DIAMANTE). - Cada terminal recebe uma sigla de 3 letras gerada por convenção própria
(
DIAMANTE → DIA,SAO GABRIEL → SAG), única dentro do diagrama (colisões resolvidas automaticamente, ex.:BARREIRO=BAR vsMOVE BARREIRO=BRR). A sigla é impressa dentro da barra da viagem (elidida e recortada à barra), servindo de rótulo compacto; o nome completo aparece no tooltip/detalhes como legenda.
Sentido por hachura (não mais por espessura)¶
Antes, a viagem de volta era desenhada mais fina que a de ida — o que a deixava sem espaço para a sigla. Agora ambos os sentidos têm altura cheia e o sentido é diferenciado por uma hachura diagonal na volta (mesma cor da linha). Assim a sigla cabe nos dois sentidos e a leitura visual fica mais clara.
Deadhead estimado pela distância entre terminais (correção de modelo)¶
A maior correção da versão. No Modo Blocos, a opção Permitir deadhead deixa um veículo encadear viagens que começam em terminais diferentes do que ele terminou. Antes, esse encadeamento não cobrava tempo de deslocamento — o veículo "se teletransportava" e a frota estimada saía menor que a real.
Aproveitando que estamos num SIG, o tempo de retorno passou a ser estimado pela geometria dos terminais:
dist_reta = haversine(terminal_B, terminal_A) # coordenadas da camada stops
tempo_retorno = (dist_reta × fator_sinuosidade) / velocidade_do_veículo_vazio
E o modelo do intervalo entre viagens virou fisicamente correto:
gap = deadhead (viagem vazia B→A) + layover (tempo ocioso no terminal)
encadeia se: layover_mín ≤ (gap − deadhead) ≤ layover_máx
— ou seja, o tempo de viagem não é mais confundido com ociosidade. Dois novos parâmetros na interface (visíveis com Permitir deadhead): velocidade do veículo vazio (padrão 25 km/h) e fator de sinuosidade/impedância reta→trajeto (padrão 1,4). Quando um terminal não tem coordenada, o deadhead é considerado instantâneo e um aviso é emitido.
Limitação assumida: a distância é em reta geodésica (não há rede de ruas carregada no plugin); o fator de sinuosidade aproxima o trajeto real. Distância de rede fica para a F2.
Achados sobre o feed (verificados no dado)¶
- 2 em cada 3 linhas só têm ida: 206 de 308 linhas têm apenas
direction_id=0(são alimentadoras — levam à estação, o retorno é outra linha/integração). As 102 bidirecionais são principalmente as troncais/diametrais (séries 1xxx/2xxx). Um diagrama "sem volta" geralmente é o dado, não bug — o leitor agora avisa quando o sentido pedido não existe nas linhas selecionadas. - Não há campo de "tipo de linha" no feed:
route_type=3(ônibus) para todas as 678 rotas. Alimentadora/troncal só dá para inferir (nº de dígitos da linha + disponibilidade de sentido).
Arquivos tocados¶
block_core.py, block_scene.py, block_diagram_dialog.py, SigBus_dialog.py,
SigBus_dialog_base.ui, metadata.txt.
v0.3 — Diagrama de Blocos (Gráfico de Alocação de Frota)¶
Introduz a feature do Diagrama de Blocos: um gráfico tempo × faixa em que
cada barra é uma viagem, clicável, com zoom/pan, construído sobre QGraphicsView
(porque o matplotlib está indisponível nesta instalação do QGIS).
Conceito de transporte¶
Um bloco é a sequência de viagens atribuída a um mesmo veículo ao longo do dia.
Como o GTFS da BHTrans não traz block_id, a alocação de frota precisa ser
inferida. A feature oferece dois modos:
- Modo Viagens (determinístico): uma faixa por (linha, sentido); viagens que se sobrepõem no tempo são empilhadas em sub-linhas (interval packing guloso) para não ficarem "encavaladas".
- Modo Blocos (inferência): encadeia viagens num mesmo veículo por heurística gulosa de frota mínima, respeitando layover (tempo de parada entre viagens) e casamento de terminais, podendo cruzar linhas (frota compartilhada). Cor por veículo; um indicador de headway (intervalo) aparece pontilhado na viagem selecionada.
Arquitetura (MVC, 3 camadas)¶
- Model —
block_core.py:Trip/Block/Schedule,ScheduleReader(leitura do GeoPackage viasqlite3, sem varrerstop_timesinteiro),BlockBuilder(inferência) eBlockDiagramTask(QgsTaskde fundo). - View —
block_scene.py(QGraphicsScene: barras, eixo, rótulos) eblock_view.py(QGraphicsView: zoom/pan, exportar PNG/SVG). - Controller —
block_diagram_dialog.py: janela própria com os controles (seleção de linhas, dia/serviço, sentido, janela de tempo, parâmetros de bloco).
Detalhes técnicos em DIAGRAMA_BLOCOS.md.
Notas de robustez¶
- Horários GTFS podem passar de 24h (
25:30:00): são tratados como segundos desde a meia-noite, não como relógio (encadear por string quebraria o pós-meia-noite). - Toda a lógica de Model/inferência foi validada fora do QGIS (stubs + SQLite sintético); a camada Qt é testada visualmente dentro do QGIS.
v0.2 — Base: GTFS → GeoPackage, demanda e relatório PDF¶
Reestruturação do projeto (de tpu/ para sig_bus/) e consolidação do núcleo de
análise de demanda.
Carga de dados¶
- GTFS embutido:
gtfs_reader.pygrava cada.txtdo feed como tabela de um GeoPackage via GDALVectorTranslateem streaming (necessário porquestop_times.txttem ~136 MB).stops/shapesviram camadas de pontos; as linhas (shapes) são montadas a partir deshapes_point. Sem depender do plugin externo GTFS Loader. calendar.txtatípico: o feed traz colunas decalendar_dates; o plugin sintetiza umcalendar.txtsemanal a partir delas ("Verificar GTFS").- Demanda (CSV): EPSG:31983, encoding windows-1252, separador
;, campos X/Y.
Análise¶
- Ligação demanda ↔ GTFS por
route_short_name(ex.:101), não porshape_id(o shape da BHTrans é numérico/sem semântica). Sentido porPC:PC=1 → ida,PC=2 → volta. - Alocação: por sentido, usa o shape dominante, projeta os embarques na parada
mais próxima e gera a camada
tramos_demandacompassageiros_acum(carga acumulada ao longo da linha). - Relatório PDF:
QgsPrintLayoutA4 paisagem, uma página por sentido, com dois mapas (carregamento graduado × clusters K-means) e gráfico de barras desenhado comQPainter(sem matplotlib).
Padrões de engenharia firmados nesta base¶
- I/O pesado em
QgsTask(trabalho na thread de fundo; mexer emQgsProjectsó na thread da GUI). - Leitura de tabelas grandes via
sqlite3com SQL agregado + índices; nunca iterar feição-a-feição emstop_times. - Nada de
UPDATEsqlite cru em tabela com geometria (quebra oST_IsEmptydo GeoPackage); usar a API QGIS/OGR. - Docstrings em PT-BR, cabeçalho GPL nos arquivos.
Antes da v0.2 (arquivo morto)¶
Protótipos e scripts standalone (incluindo Pandas_Demanda.py e a pasta tpu/) estão
arquivados em antigo/pyqgis_113-2021/ e não são a base de trabalho atual.