Skip to content

Instantly share code, notes, and snippets.

@deton
Created March 30, 2025 10:33
Show Gist options
  • Save deton/56db388abd85b0e9748ec7eea6424bf0 to your computer and use it in GitHub Desktop.
Save deton/56db388abd85b0e9748ec7eea6424bf0 to your computer and use it in GitHub Desktop.
Convert GTFS to GeoJSON for kepler.gl Trip Layer
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- modified from https://github.com/frogcat/gtfs-preview/ -->
<title>GTFS to JSON for IconTripLayer</title>
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<script src="https://unpkg.com/[email protected]/dist/jszip.min.js"></script>
<script src="https://unpkg.com/[email protected]/papaparse.min.js"></script>
</head>
<body>
<h1>Convert GTFS to JSON for <a href="https://gist.github.com/deton/57ae19aed61b03e954df43fa10aa5584">IconTripLayer</a></h1>
<div id="map">Drag and drop gtfs.zip file here.</div>
<script>
const parse = (z, n) => z.file(n).async("text").then(t => Papa.parse(t.trim(), {
header: true,
skipEmptyLines: true
}).data);
function convert(zip, fname) {
const stops = {};
parse(zip, "stops.txt").then(rows => {
rows.forEach(stop => {
stops[stop.stop_id] = [parseFloat(stop.stop_lon), parseFloat(stop.stop_lat)];
});
}).then(() => {
return parse(zip, "stop_times.txt");
}).then(rows => {
const st = {};
// TODO: filter trip_id by service_id(trips.txt) or date(calendar.txt)
rows.forEach(row => {
if (st[row.trip_id] === undefined) {
st[row.trip_id] = [];
}
const seq = parseInt(row.stop_sequence);
st[row.trip_id].push({
seq: seq,
stop_id: row.stop_id,
timestamp: row.arrival_time,
});
if (row.departure_time && row.departure_time !== row.arrival_time) {
st[row.trip_id].push({
seq: seq + 0.5,
stop_id: row.stop_id,
timestamp: row.departure_time,
});
}
});
return st;
}).then(stopTimes => {
const result = [];
const now = new Date();
const todayms = new Date().setHours(0, 0, 0, 0);
Object.keys(stopTimes).forEach(trip_id => {
const st = stopTimes[trip_id].sort((a, b) => a.seq - b.seq);
result.push({
vendor: trip_id,
path: st.map(x => stops[x.stop_id]),
timestamps: st.map(x => {
return (now.setHours(...x.timestamp.split(':'), 0) - todayms) / 1000;
}),
});
});
const outfname = fname.replace(/\.zip$/, '.json');
const outfile = new File([JSON.stringify(result)], outfname, {
type: 'application/json'
});
const a = document.createElement('a');
a.href = URL.createObjectURL(outfile);
a.download = outfile.name;
a.click();
URL.revokeObjectURL(a.href);
}).catch(console.error);
}
(function (me) {
me.addEventListener("dragover", ev => {
ev.preventDefault();
ev.stopPropagation();
});
me.addEventListener("drop", ev => {
ev.preventDefault();
ev.stopPropagation();
const promises = Array.from(ev.dataTransfer.files).map(file => {
return JSZip.loadAsync(file).then(zip => {
convert(zip, file.name);
});
});
Promise.all(promises).catch(console.error);
});
})(document.getElementById("map"));
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- modified from https://github.com/frogcat/gtfs-preview/ -->
<title>GTFS to GeoJSON for kepler.gl Trip Layer</title>
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
<script src="https://unpkg.com/[email protected]/dist/jszip.min.js"></script>
<script src="https://unpkg.com/[email protected]/papaparse.min.js"></script>
</head>
<body>
<h1>Convert GTFS to GeoJSON for kepler.gl Trip Layer</h1>
<div id="map">Drag and drop gtfs.zip file here.</div>
<script>
const parse = (z, n) => z.file(n).async("text").then(t => Papa.parse(t.trim(), {
header: true,
skipEmptyLines: true
}).data);
function convert(zip, fname) {
const stops = {};
parse(zip, "stops.txt").then(rows => {
rows.forEach(stop => {
stops[stop.stop_id] = [parseFloat(stop.stop_lon), parseFloat(stop.stop_lat)];
});
}).then(() => {
return parse(zip, "stop_times.txt");
}).then(rows => {
const st = {};
// TODO: filter trip_id by service_id(trips.txt) or date(calendar.txt)
rows.forEach(row => {
if (st[row.trip_id] === undefined) {
st[row.trip_id] = [];
}
const seq = parseInt(row.stop_sequence);
st[row.trip_id].push({
seq: seq,
stop_id: row.stop_id,
timestamp: row.arrival_time,
});
if (row.departure_time && row.departure_time !== row.arrival_time) {
st[row.trip_id].push({
seq: seq + 0.5,
stop_id: row.stop_id,
timestamp: row.departure_time,
});
}
});
return st;
}).then(stopTimes => {
const result = [];
const now = new Date();
Object.keys(stopTimes).forEach(trip_id => {
const st = stopTimes[trip_id].sort((a, b) => a.seq - b.seq);
const coords = st.map(x => {
const ts = (now.setHours(...x.timestamp.split(':'), 0)) / 1000;
// lon,lat,alt,timestamp for kepler.gl
return [...stops[x.stop_id], 0, ts];
});
result.push({
type: 'Feature',
properties: {
trip_id: trip_id,
},
geometry: {
type: 'LineString',
coordinates: coords,
},
});
});
const geojson = {type: 'FeatureCollection', 'features': result};
const outfname = fname.replace(/\.zip$/, '.geojson');
const outfile = new File([JSON.stringify(geojson)], outfname, {
type: 'application/geo+json'
});
const a = document.createElement('a');
a.href = URL.createObjectURL(outfile);
a.download = outfile.name;
a.click();
URL.revokeObjectURL(a.href);
}).catch(console.error);
}
(function (me) {
me.addEventListener("dragover", ev => {
ev.preventDefault();
ev.stopPropagation();
});
me.addEventListener("drop", ev => {
ev.preventDefault();
ev.stopPropagation();
const promises = Array.from(ev.dataTransfer.files).map(file => {
return JSZip.loadAsync(file).then(zip => {
convert(zip, file.name);
});
});
Promise.all(promises).catch(console.error);
});
})(document.getElementById("map"));
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment