Arquitetura — Edição de GTFS (PyQGIS)¶
Projeto: SIG-Bus (plugin QGIS) · Data: 2026-06-19
Branch: feature/editar-gtfs
Documento irmão (referência de estilo): ARQUITETURA_DIAGRAMA.md
Funcionalidade nova: editar parâmetros de um GTFS já carregado e exportar uma nova
versão em .zip, pronta para qualquer plataforma (Google Maps, validadores, etc.).
Segue o mesmo padrão MVC em camadas do Diagrama de Blocos, para ser uma extensão
do plugin e não uma ilha.
1. Decisões de projeto (tomadas em 2026-06-19)¶
Estas escolhas guiam toda a arquitetura abaixo:
- Motor de edição: híbrido. O plugin guia (combobox + filtro), protege os campos de chave (IDs/FKs) e valida; a edição em si acontece na tabela de atributos nativa do QGIS (ganhamos buffer de edição, undo/redo, busca e calculadora de campos de graça) e, para o espacial, na edição de vértices do canvas.
- Onde editar: cópia de trabalho. As edições acontecem num
feed_edit.gpkg, cópia dofeed.gpkgda análise. Uma edição pela metade nunca corrompe a análise, e dá para descartar tudo. - Integridade: proteger chaves + validar na exportação. Campos de ID ficam
read-only; a checagem de integridade referencial e de formato roda no momento de
gerar o
.zip. - Exportação: normalizar tudo. A saída é GTFS o mais aderente possível à spec
(ordem canônica de colunas,
calendar.txt/calendar_dates.txtcorretos, remoção de campos não-padrão). Isso conserta a esquisitice do feed da BHTrans (cujocalendar.txtvem em formato decalendar_dates). stop_times: só subconjunto filtrado. Nunca carregar a tabela inteira (~136 MB / milhões de linhas) na GUI — edita-se por linha/viagem via filtro. A exportação reemite a tabela toda via streaming.
2. Visão geral (MVC em três camadas)¶
┌─────────────────────────────────────────────────────────────────────┐
│ INTERFACE (Controller) gtfs_edit_dialog.py + aba na .ui │
│ - combobox Tabela / combobox Campo-filtro │
│ - botões: Entrar no modo edição · Abrir p/ edição · Editar no mapa │
│ Validar · Descartar · Exportar .zip │
│ - trava campos protegidos (editFormConfig.setReadOnly) │
│ - abre tabela de atributos nativa / ativa edição de vértices │
└───────────────┬───────────────────────────────────┬──────────────────┘
│ parâmetros │ usa
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────────┐
│ CORE (Model) │ │ SPEC GTFS │
│ gtfs_edit_core.py │ ───► │ gtfs_schema.py │
│ - WorkingCopy (criar/descar)│ usa │ - colunas canônicas + ordem │
│ - GtfsValidator (FK+formato)│ │ - obrigatórias/opcionais │
│ - GtfsExporter (QgsTask) │ │ - editável vs. protegida (IDs) │
└───────────────┬──────────────┘ └──────────────────────────────────┘
│ lê/grava
▼
┌───────────────┐ cópia de ┌───────────────┐
│ feed.gpkg │ ───────────► │ feed_edit.gpkg │ ──► feed_novo.zip
│ (análise) │ │ (edição) │ (export normaliz.)
└───────────────┘ └───────────────┘
gtfs_schema.py é a fonte única da verdade. Alimenta ao mesmo tempo: (a) a
whitelist de campos editáveis na UI, (b) a validação, e (c) a ordem das colunas na
exportação normalizada. Mexer na spec num lugar só propaga para os três usos.
3. SPEC — gtfs_schema.py¶
Camada sem dependência de Qt nem de PyQGIS — só dados. Descreve a especificação GTFS (ver https://gtfs.org/documentation/schedule/reference/) no recorte que o plugin usa. Estrutura sugerida, por arquivo GTFS:
# Esboço conceitual (não é o código final).
GTFS_FILES = {
"routes": {
"required": True,
"columns": [ # ordem canônica de saída
Col("route_id", editable=False, required=True), # chave
Col("agency_id", editable=False, required=False), # FK
Col("route_short_name", editable=True, required=False),
Col("route_long_name", editable=True, required=False),
Col("route_type", editable=True, required=True, enum={0..12}),
...
],
"foreign_keys": [("agency_id", "agency", "agency_id")],
},
"trips": {... "foreign_keys": [
("route_id", "routes", "route_id"),
("service_id", "calendar", "service_id"),
("shape_id", "shapes", "shape_id"),
]},
"stop_times": {...}, "stops": {...}, "calendar": {...}, "shapes": {...},
"agency": {...}, "calendar_dates": {...},
}
Campos protegidos (editable=False): toda chave primária e estrangeira
(*_id). Editar IDs quebraria a integridade referencial — fora do escopo desta
feature (seria uma operação de "renomear entidade", outra história).
4. CORE (Model) — gtfs_edit_core.py¶
Camada de lógica de dados + PyQGIS, sem widgets. Três responsabilidades:
4.1 WorkingCopy¶
enter(): copiafeed.gpkg→feed_edit.gpkg(ao lado, mesmo diretório). Se já existir, pergunta retomar/recriar (decisão da UI).discard(): apagafeed_edit.gpkg.is_active(): existe uma cópia de trabalho?- Cópia é de arquivo (SQLite é um único arquivo) — barato e atômico.
4.2 GtfsValidator¶
Roda na exportação (decisão 3). Reporta erros (fatais → bloqueiam) e avisos
(→ apenas alertam), via iface.messageBar() + QgsMessageLog (LOG_TAG='SIG-Bus').
- Integridade referencial (das
foreign_keysdo schema): todatrip.route_id∈routes;trip.service_id∈calendar/calendar_dates;trip.shape_id∈shapes;stop_times.trip_id∈trips;stop_times.stop_id∈stops. Implementação por SQL agregado (LEFT JOIN ... WHERE x IS NULL), nunca feição-a-feição. - Formato: horários como segundos podendo passar de 24h (
25:30:00); datasYYYYMMDD; lat/lon em faixas válidas; enums (route_type,direction_id,exception_type).
4.3 GtfsExporter (subclasse de QgsTask)¶
Lê do feed_edit.gpkg e escreve um .zip GTFS normalizado (decisão 4).
Trabalho pesado em run() (thread de fundo); só sinalização/UI em finished().
- Para cada arquivo do schema: emite as colunas na ordem canônica, descartando campos não-padrão.
shapes.txt: regenerado a partir dos vértices da camada de linhasshapes(cada vértice → uma linhashape_id, shape_pt_lat, shape_pt_lon, shape_pt_sequence, sequência recriada). Lossless: cada ponto doshapes_pointoriginal já vira um vértice embuild_shapes_line.stops.txt:stop_lon/stop_latlidos da geometria do ponto.calendar: geracalendar.txtsemanal (a partir do que_calendar_from_datesinfere) +calendar_dates.txtpara as exceções — conserta o feed da BHTrans.stop_times.txt: escrito via streaming (cursorsqlite3→csv.writer), sem carregar a tabela em memória.- Empacota tudo num
.zip(zipfile, um.txtpor entrada).
5. INTERFACE (Controller) — gtfs_edit_dialog.py + aba na .ui¶
Nova aba "Edição GTFS" no diálogo principal (ou janela própria, a decidir na implementação). Fluxo:
Aba "Edição GTFS"
├─ [Entrar no modo edição] → WorkingCopy.enter(): feed.gpkg → feed_edit.gpkg
├─ Combobox "Tabela": trips ▾ (só as editáveis do schema)
├─ Combobox "Campo/filtro": trip_headsign ▾
├─ [Abrir para edição] → carrega a camada do feed_edit.gpkg em modo de
│ edição na TABELA DE ATRIBUTOS NATIVA; IDs travados
│ via editFormConfig.setReadOnly(idx)
├─ (stops/shapes) [Editar no mapa] → startEditing() + ferramenta de vértices
├─ (stop_times) → aplica subsetString por trip_id/linha ANTES de abrir
├─ [Validar] → GtfsValidator (relatório no messageBar/log)
├─ [Descartar] → WorkingCopy.discard()
└─ [Exportar .zip] → GtfsExporter (QgsTask) → feed_novo.zip
Caminhos de edição¶
- Não-espacial (híbrido): combobox Tabela → Campo/filtro → abre a camada na tabela de atributos nativa, com os IDs em read-only. A calculadora de campos nativa cobre edições em massa.
- Espacial (canvas):
stops/shapes→startEditing()+ edição de vértices. Passa pela API do QGIS/OGR → respeita a regra de ouro do projeto: nuncaUPDATEsqlite cru em tabela com geometria (quebraST_IsEmpty). stop_times(filtrado):setSubsetString("trip_id IN (...)")por linha/viagem antes de abrir — nunca a tabela inteira na GUI.
6. Padrões obrigatórios herdados (CLAUDE.md)¶
- I/O pesado em
QgsTask(run()no fundo;QgsProject/camadas só emfinished()). - Tabelas grandes via
sqlite3com SQL agregado + índices — nunca iterarstop_timesfeição-a-feição. - Sem
UPDATEsqlite cru em tabela com geometria → usar API QGIS/OGR. - Feedback via
iface.messageBar()eQgsMessageLog(LOG_TAG='SIG-Bus'). - Docstrings em PT-BR, cabeçalho GPL nos arquivos
.py.
7. Ordem de implementação (fatias finas e testáveis)¶
Fecha o ciclo editar → exportar cedo, com tabelas simples, antes de canvas e
stop_times:
- [x] Esqueleto — aba "Edição GTFS" +
WorkingCopy(entrar/descartar) +gtfs_schemacom 1–2 tabelas. - [x] Edição não-espacial de uma tabela simples (ex.:
routes→route_short_name/route_long_name) na tabela nativa, IDs travados. - [x] Exportador normalizado (tabelas simples + calendar correto) → ciclo completo ponta a ponta.
- [x] Edição espacial (
stops, depoisshapes). - [x]
stop_timesfiltrado. - [x]
GtfsValidatorcompleto na exportação.
8. Resoluções e decisões da implementação¶
- Modelo de edição de
shapes: A edição de traçados ocorre exclusivamente na camada de linhasshapes(gerada pelo QGIS a partir dos pontos do feed). A camada de pontosshapes_pointoriginal não é exposta na interface de edição para evitar inconsistências. Durante a exportação, o arquivoshapes.txté regenerado extraindo cada vértice da linha como um ponto individual, ordenando e numerando-os sequencialmente porshape_pt_sequence. - Tratamento de
agency_idem feed de agência única: Emgtfs_schema.py, o campoagency_idé marcado como opcional (required=False) tanto na tabelaagencyquanto na tabelaroutes. O validadorGtfsValidatorsó exige a validação de integridade referencialroutes.agency_id -> agency.agency_idpara registros em que o campo esteja preenchido. Se o feed omitir ou deixar em branco esses campos, a validação é concluída sem gerar erros. - Interface e layout: A funcionalidade de edição foi integrada como uma nova aba ("Edição GTFS") no diálogo principal do SIG-Bus.
- Retomada de edição: Ao reentrar na aba com um
feed_edit.gpkgjá existente, o plugin identifica a cópia de trabalho ativa e retoma as edições anteriores, oferecendo também a opção de descartá-la. - Destino do ZIP: O arquivo ZIP exportado é gravado em local escolhido pelo usuário por meio do diálogo nativo "Salvar como".