Skip to content

Commit d4228df

Browse files
authored
Add GTFS Features Adoption Tracker (#670)
* Add feature tracker * Update gtfs_tracker.js * update design * Update gtfs_tracker.js
1 parent 09d7fa4 commit d4228df

9 files changed

Lines changed: 400 additions & 1 deletion

File tree

config/en/mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ theme:
4141
icon:
4242
repo: fontawesome/brands/github
4343
extra_css:
44+
- assets/stylesheets/gtfs_tracker.css
4445
- assets/stylesheets/extra.css
4546
- assets/stylesheets/footer.css
4647
extra_javascript:
48+
- assets/javascript/gtfs_tracker.js
4749
- assets/javascript/anchor-redirects.js
4850
- assets/javascript/languageSwitchUrls.js
4951
extra:

docs/en/assets/Google.png

180 KB
Loading

docs/en/assets/aubin.png

114 KB
Loading

docs/en/assets/motis.png

8.19 KB
Loading

docs/en/assets/opentripplanner.png

16.4 KB
Loading

docs/en/assets/transitapp.png

24.3 KB
Loading

docs/en/getting-started/features/overview.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ These essential features form the core of a GTFS feed. They are the minimal elem
4747

4848
</div>
4949

50+
<div class="gtfs-feature-tracker" data-category="Base"></div>
51+
5052
## Base add-ons
5153
These features enhance a GTFS dataset, improving rider experience and facilitating collaboration between agencies, vendors, and data re-users. They may involve adding new fields to existing files or creating new files.
5254

@@ -78,7 +80,7 @@ These features enhance a GTFS dataset, improving rider experience and facilitati
7880

7981
- :material-plus-box-multiple-outline:{ .lg .middle } __Headsigns__
8082

81-
Communicate the signage used by vehicles indicating the trips destination.
83+
Communicate the signage used by vehicles indicating the trip's destination.
8284

8385
[:octicons-arrow-right-24: Learn more](../base_add-ons/#headsigns)
8486

@@ -125,6 +127,7 @@ These features enhance a GTFS dataset, improving rider experience and facilitati
125127
[:octicons-arrow-right-24: Learn more](../base_add-ons/#stop-access)
126128
</div>
127129

130+
<div class="gtfs-feature-tracker" data-category="Base add-ons"></div>
128131

129132
## Accessibility
130133
Accessibility features provide essential information for people with disabilities to access the service.
@@ -151,6 +154,7 @@ Accessibility features provide essential information for people with disabilitie
151154

152155
</div>
153156

157+
<div class="gtfs-feature-tracker" data-category="Accessibility"></div>
154158

155159
## Fares
156160
GTFS can model various fare structures, such as zone, distance, or time-of-day based fares. It informs riders of trip prices and payment methods.
@@ -213,6 +217,7 @@ GTFS can model various fare structures, such as zone, distance, or time-of-day b
213217

214218
</div>
215219

220+
<div class="gtfs-feature-tracker" data-category="Fares"></div>
216221

217222
## Pathways
218223

@@ -252,6 +257,8 @@ Pathways features allows to model large transit stations, so that riders are gui
252257

253258
</div>
254259

260+
<div class="gtfs-feature-tracker" data-category="Pathways"></div>
261+
255262
## Flexible services
256263
Flexible services, or demand-responsive services, that do not follow regular schedules or fixed routes.
257264

@@ -288,3 +295,5 @@ Flexible services, or demand-responsive services, that do not follow regular sch
288295
[:octicons-arrow-right-24: Learn more](../flexible_services/#fixed-stops-demand-responsive-services)
289296

290297
</div>
298+
299+
<div class="gtfs-feature-tracker" data-category="Flexible services"></div>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
(function () {
2+
const CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSu9_3lyF9caXrDdlGCtO1Bg17Uhkh_L9l-REYkYVUINvrEEaVwrx1mSZ--_iKAGcJ2x8bFBzYHVU74/pub?output=csv';
3+
const FIELDS_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSu9_3lyF9caXrDdlGCtO1Bg17Uhkh_L9l-REYkYVUINvrEEaVwrx1mSZ--_iKAGcJ2x8bFBzYHVU74/pub?gid=1583091937&single=true&output=csv';
4+
5+
const CONSUMERS = [
6+
{ id: 'google', label: 'Google' },
7+
{ id: 'transitapp', label: 'Transit' },
8+
{ id: 'motis', label: 'MOTIS' },
9+
{ id: 'OpenTripPlanner', label: 'OTP' },
10+
{ id: 'aubin', label: 'Aubin' },
11+
];
12+
13+
let knownFields = [];
14+
15+
const ICONS = {
16+
check: `<svg class="gft-icon icon-success" viewBox="0 0 24 24"><path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>`,
17+
x: `<svg class="gft-icon icon-danger" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/></svg>`,
18+
clock: `<svg class="gft-icon icon-warning" viewBox="0 0 24 24"><path d="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.53 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z"/></svg>`,
19+
partial: `<svg class="gft-icon" viewBox="0 0 24 24" fill="#FFE296"><path d="M12,2A10,10 0 0,0 12,22A10,10 0 0,0 12,2M12,4V20A8,8 0 0,1 12,4Z"/></svg>`,
20+
planned: `<svg class="gft-icon icon-info" viewBox="0 0 24 24"><path d="M4,15V9H12V4.16L19.84,12L12,19.84V15H4Z"/></svg>`,
21+
na: `<svg class="gft-icon" viewBox="0 0 24 24" fill="#9aa0a6"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.86.64-3.57 1.71-4.93L16.93 18.29C15.57 19.36 13.86 20 12 20zm6.29-4.71L7.71 5.71C9.07 4.64 10.78 4 12 4c4.41 0 8 3.59 8 8 0 1.86-.64 3.57-1.71 4.93z"/></svg>`
22+
};
23+
24+
function getStatusMeta(raw) {
25+
const n = (raw || '').toLowerCase().trim();
26+
if (n.startsWith('yes - for every feed')) return { label: 'Every feed', icon: ICONS.check, cls: 'icon-success' };
27+
if (n.startsWith('yes')) return { label: 'Yes', icon: ICONS.check, cls: 'icon-success' };
28+
if (n.startsWith('yes - for some feeds')) return { label: 'Some feeds', icon: ICONS.clock, cls: 'icon-warning' };
29+
if (n.includes('some fields are ignored')) return { label: 'Partial', icon: ICONS.partial, cls: 'icon-warning' };
30+
if (n.includes('partial') || n.includes('ignored')) return { label: 'Partial', icon: ICONS.partial, cls: 'icon-neutral' };
31+
if (n === 'test in progress') return { label: 'In progress', icon: ICONS.clock, cls: 'icon-warning' };
32+
if (n === 'integration planned') return { label: 'Planned', icon: ICONS.planned, cls: 'icon-info' };
33+
if (n.startsWith('no')) return { label: 'No', icon: ICONS.x, cls: 'icon-danger' };
34+
if (n === 'n/a' || n === 'unknown') return { label: 'N/A', icon: ICONS.na, cls: 'icon-neutral' };
35+
return { label: '—', icon: ICONS.na, cls: 'icon-neutral' };
36+
}
37+
38+
function parseCSV(text) {
39+
const rows = []; let row = [], field = '', inQ = false;
40+
for (let i = 0; i < text.length; i++) {
41+
const ch = text[i];
42+
if (inQ) { if (ch === '"' && text[i + 1] === '"') { field += '"'; i++; } else if (ch === '"') inQ = false; else field += ch; }
43+
else { if (ch === '"') inQ = true; else if (ch === ',') { row.push(field); field = ''; } else if (ch === '\n') { row.push(field); rows.push(row); row = []; field = ''; } else if (ch !== '\r') field += ch; }
44+
}
45+
if (field || row.length) { row.push(field); rows.push(row); }
46+
return rows;
47+
}
48+
49+
function formatContent(text) {
50+
if (!text) return '';
51+
let html = text;
52+
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>');
53+
knownFields.forEach(field => {
54+
if (!field) return;
55+
const regex = new RegExp(`\\b${field}\\b`, 'g');
56+
html = html.replace(regex, `<code class="gft-field-badge">${field}</code>`);
57+
});
58+
return html.replace(/\n/g, '<br>');
59+
}
60+
61+
window.showGftDetails = (title, content) => {
62+
const m = document.getElementById('gft-modal');
63+
const t = document.getElementById('gft-modal-title');
64+
const b = document.getElementById('gft-modal-body');
65+
if (m && t && b) {
66+
t.innerText = title;
67+
b.innerHTML = formatContent(content);
68+
m.style.display = 'flex';
69+
}
70+
};
71+
72+
function renderTracker(el, features) {
73+
const cat = el.dataset.category;
74+
const catRows = features.filter(f => f.category === cat);
75+
if (!catRows.length) return;
76+
77+
const tableRows = catRows.map(f => {
78+
const cells = CONSUMERS.map(c => {
79+
const s = f.support[c.id];
80+
const m = getStatusMeta(s.rawStatus);
81+
const hasDet = s.details && s.details.trim().length > 0;
82+
return `<td class="gft-cell">
83+
<div class="gft-badge ${m.cls}">
84+
<span class="gft-icon-anchor">${m.icon}</span>
85+
<span class="gft-label">${m.label}</span>
86+
${hasDet ? `<i class="gft-info-btn" onclick="showGftDetails('${f.name.replace(/'/g, "\\'")}${c.label}', \`${s.details.replace(/`/g, '\\`').replace(/\\/g, '\\\\')}\`)">i</i>` : ''}
87+
</div>
88+
</td>`;
89+
}).join('');
90+
return `<tr class="gft-row"><td class="gft-feature-name" title="${f.name}">${f.name}</td>${cells}</tr>`;
91+
}).join('');
92+
93+
el.innerHTML = `
94+
<div class="gft-wrapper">
95+
<div class="gft-table-wrap">
96+
<table class="gft-table" style="width:100%; border-collapse:collapse; font-family:sans-serif;">
97+
<thead>
98+
<tr style="background:#f9fafb;">
99+
<th style="border:1px solid #eee; padding:8px;"></th>
100+
${CONSUMERS.map(c => `
101+
<th style="border:1px solid #eee; padding:8px;">
102+
<div class="gft-consumer-header" style="display:flex; flex-direction:column; align-items:center; gap:4px;">
103+
<img src="/assets/${c.id}.png" alt="" style="height:16px; width:auto;" onerror="this.style.display='none'">
104+
<span style="font-size:11px;">${c.label}</span>
105+
</div>
106+
</th>`).join('')}
107+
</tr>
108+
</thead>
109+
<tbody>${tableRows}</tbody>
110+
</table>
111+
</div>
112+
<div class="gft-footer-link">
113+
Learn more about the <a href="https://mobilitydatabase.org/gtfs-feature-tracker" target="_blank">GTFS Features Tracker</a>
114+
</div>
115+
</div>`;
116+
}
117+
118+
function boot() {
119+
if (!document.getElementById('gft-modal')) {
120+
document.body.insertAdjacentHTML('beforeend', `
121+
<div id="gft-modal" class="gft-modal">
122+
<div class="gft-modal-content">
123+
<span class="gft-modal-close" onclick="this.parentElement.parentElement.style.display='none'">&times;</span>
124+
<span id="gft-modal-title" class="gft-modal-title"></span>
125+
<div id="gft-modal-body" class="gft-modal-body"></div>
126+
</div>
127+
</div>`);
128+
}
129+
130+
Promise.all([
131+
fetch(FIELDS_URL).then(r => r.text()),
132+
fetch(CSV_URL).then(r => r.text())
133+
]).then(([fieldsText, dataText]) => {
134+
const fieldRows = parseCSV(fieldsText);
135+
knownFields = fieldRows.slice(1).map(r => r[0]).filter(name => name && name.length > 2);
136+
137+
const rows = parseCSV(dataText);
138+
const headers = rows[0];
139+
const data = rows.slice(1).filter(r => r[0]).map(r => {
140+
const support = {};
141+
CONSUMERS.forEach(c => {
142+
const useIdx = headers.indexOf(c.id + '_use');
143+
const detIdx = headers.indexOf(c.id + '_details');
144+
support[c.id] = { rawStatus: r[useIdx] || '', details: r[detIdx] || '' };
145+
});
146+
return { name: r[0], category: r[1], support };
147+
});
148+
149+
document.querySelectorAll('.gtfs-feature-tracker').forEach(el => renderTracker(el, data));
150+
}).catch(err => console.error("GFT Error:", err));
151+
}
152+
153+
if (typeof document$ !== 'undefined') document$.subscribe(boot); else document.addEventListener('DOMContentLoaded', boot);
154+
})();

0 commit comments

Comments
 (0)