Schlagwort-Archive: GTFS

Adding shapes to GTFS feeds with pfaedle

Headway frequency mapping in R. Requires shapes.txt

Three years ago, I wrote a little piece about how we cleaned up SWU’s GTFS feed.

I nonchalantly added that adding shapes and Conveyal’s GTFS editor would be a topic for another time, but never came around writing about that. I do not use the GTFS editor anymore, but Patrick Brosi’s pfaedle tool is still invaluable if your GTFS feed does not come equipped with a functional shapes.txt.

I had described the problem and where to find the proper tools back in early 2020 right at the intersection of my activism and public administration work. With the regional transit area spanning two Bundeslaender, there are some pitfalls left, however. Hence, a short primer.

Ingredients:

  • One Linux machine, whatever the flavor. Be it a VM or an old Laptop, it hardly matters. It shouldn’t be the slowest available machine, though, and it should come with a decent amount of RAM (the machine I’m using has 8 GiB). And if you go the germany-latest route (see below), about 100 GiB of hard disk space are required.
  • cmake, gcc (>4.9), git, wget, bzip2: sudo apt install cmake gcc git wget bzip2
  1. Get pfaedle, which is pretty much following the steps outlined in the github repo:
git clone --recurse-submodules https://github.com/ad-freiburg/pfaedle
mkdir build && cd build
cmake ..
make -j4
# optionally:
make install

2. Navigate to the folder where you store your unzipped(!) GTFS feed you want to add shapes to.

3. Get the proper OSM files. Since we are working with Ulm and Neu-Ulm, we’d either need a download of the metropolitan area of both cities, or download and merge the extracts for Bavaria and Baden-Wuerttemberg… or download and use the extract for the whole of Germany :shrug:

# Whole of Germany
wget https://download.geofabrik.de/europe/germany-latest.osm.bz2
bunzip2 germany-latest.osm.bz2

# Merge, requires osmium-tool: apt install osmium-tool
wget https://download.geofabrik.de/europe/germany/bayern-latest.osm.bz2
wget https://download.geofabrik.de/europe/germany/baden-wuerttemberg-latest.osm.bz2
bunzip2 bayern-latest.osm.bz2 && bunzip2 baden-wuerttemberg-latest.osm.bz2
osmium merge baden-wuerttemberg-latest.osm bayern-latest.osm -o merged.osm

Beware: Unzipping the GTFS feeds takes ages, especially the germany-latest. Expect a file exceeding 70 GiB and quite some decompression time. My laptop takes about 4–5 minutes for each Bundesland to unpack.

All that is left to do now is to let pfaedle do it’s work: pfaedle -D -x merged.osm .
After completion (and again, using it with germany-latest.osm takes quite a lot of time), a new folder gtfs-out is created. Test the results with your usual testing suites, ZIP it up, and off you go.

Fahrplandaten, Zwischenstand 2020

Seit September 2006 gibt es das GTFS-Format fuer Soll-Fahrplandaten des oeffentlichen Verkehrs, entstanden mehr als Zufalls-Seitenprojekt aus einer Kooperation zwischen einem Google-Maps-Entwickler und dem Portlander Verkehrsverbund. Gedacht war es als Standard-Austauschformat, um Fahrplandaten in verschiedene beliebige Verkehrsauskuenfte zu integrieren – auch Google Maps, aber eben auch viele andere Anwendungen.

In Deutschland dauerte es sehr lange, bis GTFS oder das sehr viel umfangreichere NeTEx-Format halbwegs breit als Grundlage fuer Open-Data-Fahrplaene angeboten wurde. Deutsche Verkehrsverbuende sind vielfach bis heute der Ueberzeugung, dass nur sie selbst „richtige“ Auskuenfte geben koennen. Das wuerde ich generell sowieso in Frage stellen, aber das ist gaengige Policy, und auch in der VDV-Schrift 7030 „Open Service“ in eine wirklich haarstraeubende Sammlung zurschaugestellter Ahnungslosigkeit und Falschdarstellungen zusammengefasst. Teile der Geschichte dazu lassen sich in meiner Diplomarbeit von 2014 (sic) nachlesen.

Sehr sehr langsam tat sich aber etwas in Deutschland – auch weil Anfang Dezember 2019 die Delegierte Verordnung (EU) 1926/2017 die Auslieferung aller(!) Soll-Fahrplandaten ueber einen Nationalen Accesspoint zur Pflicht machte. Die Verbuende, die sich lange Zeit wehrten, mussten also nun ihre Fahrplaene offenlegen, auch zur Nachnutzung durch Dritte. Die Open-Data-Landkarte, auf der 2013 nur der VBB und die vergleichsweise winzigen Stadtwerke Ulm zu finden waren, faerbte sich nach und nach:

Wenn auch viel zu spaet und sehr schleppend und weit entfernt von den skandinavischen Best-Practise-Beispielen, die hier meilenweit voraus sind: Es tat sich was.

Das schien nun also der Anlass fuer diverse Diensteanbieter zu sein, nun auch die notorisch verspaeteten deutschen Verbuende in ihre Auskuenfte einzubinden. So auch Apple Maps, die offenbar nun einige Regionen mehr in ihre Auskunft aufnahmen – was zu diesem denkwuerdigen Artikel auf nordbayern.de fuehrte, der einem nochmal plakativ vor Augen fuehrt, auf welchem unterirdischen Niveau wir 2020 noch ueber intermodale Verkehrsauskuenfte reden. Beste Zitate:

Die Daten stammen, so behauptet es Apple in der App, von der Verkehrs-Aktiengesellschaft Nürnberg (VAG) und dem Verkehrsverbund Großraum Nürnberg (VGN). Dort allerdings wundert man sich. „Es gibt keine vertragliche Vereinbarung (…) zu einer Überlassung von Fahrplandaten“, sagt VGN-Sprecher Manfred Rupp. „Wir wissen auch nicht, woher Apple die Fahrplandaten zu den VGN-Linien hat.“ Man könne, sagt Rupp, nur spekulieren – und sei dabei, die technischen Vorgänge aufzuarbeiten.

Das ist wirklich bemerkenswert, denn auf der Website des VGN selber findet sich ein Punkt „Soll-Fahrplaene im GTFS-Format“, und ueber den Standardvertrag der Creative-Commons-Lizenz ist auch die vertragliche Ueberlassung von Fahrplandaten ganz klar geregelt (Ueberlegungen, ob es sich bei Fahrplandaten ueberhaupt um ein urheberrechtlich geschuetztes Werk handelt, auf das auf dem UrhG basierende Lizenzen anzuwenden sein koennen, seien mal aussen vor gelassen).

Im weiteren Text wird zunaechst behauptet, Apple erwecke den Eindruck, dass es sich um Echtzeitdaten (also unter Einberechnung von Verspaetungen) handle. „Das könne aber gar nicht sein […] Die liegen nicht einmal mehr auf unseren Servern“ wird der VGN zitiert. Einen Absatz weiter wird jedoch zurueckgerudert, dass es sich um „aeltere“ Soll-Fahrplandaten unklaren Alters handelt.

Diese Reaktion ist fuer mich wirklich erschreckend. Ziel des VGN muesste es sein, eine stabile Quelle tagesaktueller Soll-Fahrplaene bereitzustellen. Sollte Apple wirklich den GTFS-Feed des VGN vom Dezember 2019 benutzen und dieser nicht aktuell sein, liegt das Problem glasklar auf Seiten des Verbunds. Er hat dafuer zu sorgen, dass Dritte Daten in guter Qualitaet nutzen koennen – spaetestens seit der Delegierten Verordnung. Und die vielfach als Argument angefuehrten Echtzeit-Ist-Daten sind vielmehr ein gewichtiges Argument, spaetestens jetzt die Vorbereitungen zu treffen, auch diese Daten fuer Dritte nutzbar ausspielen zu koennen. Denn Echtzeit und Open Data schliessen sich nicht aus – wenn man denn wie die Skandinavier auf die richtigen Formate setzt, anstatt dem VDV-Unsinn rund um den sogenannten „Open Service“ nachzulaufen.

Die Uhr tickt indes. Spaetestens im Dezember 2023 ist die Ausspielung der dynamischen (Echtzeit) Daten gemaess DV 1926/2017 EU-weit gefordert und nicht mehr nur optional. Ich bin gespannt, ob die deutschen Verbuende das auch so grandios verkacken werden wie bei den Soll-Daten.

PS: Wer Lobbyarbeit machen moechte: Die Karte oben stammt aus dem 2017 als Spassprojekt gestarteten One-Click-Lobby-Projekt rettedeinennahverkehr.de – dort kannst du dich mit wenig Aufwand an deinE LandraetIn oder OberbuergermeisterIn wenden.

Cleaning up the SWU GTFS feed with pygtfs and sqlite3

For a Top Secret Project™ we are currently using SWU’s official GTFS feed. Since we had to clean up the feed a bit and ran into some errors, I thought it best to document our workflow to save it for posterity – it might even help others ;

Step 1: Loading into sqlite

For me, the first step is always to load the feed into sqlite3, since it makes it easier to follow through with all the manipulations I have in mind for later.

My current tool of choice is pygtfs, which can be installed through pip. If you don’t have sqlite installed, you might want to do that now, too.

pip3 install pygtfs
apt install sqlite3

pygtfs brings along the handy gtfs2db tool, which allows for importing your feed into a database right from the command line:

gtfs2db append ~/Downloads/gtfs.zip gtfs.db 

This command will, however, fail after importing all the records and trying to write it to the database, because it can’t read a datetime (ValueError: time data “ does not match format ‚%Y%m%d‘). After some digging I found the missing feed_start_date and feed_end_date in feed_info.txt to be the culprit. According to spec, they are merely optional, but the gtfs2db script seems to depend on it. Nothing a quick edit in the text editor of your choice can’t fix. All there is left to do is to retry the import, and open a sqlite3 session with the newly created database. I am in the habit of switching on the headers and into column mode right from the get-go.

sqlite3 -header -column gtfs.db 

Step 2: Exploring the stops table

For our project, we wanted to clean up the stops table a bit. First of all, in violation of the spec, in the SWU feed the stop_id is multiply assigned to every stop point („Haltepunkt“) within a stop („Haltestelle“). We can find this rather easily:

SELECT stop_name, count(stop_code) FROM stops GROUP BY stop_code ORDER BY count(stop_code) DESC limit 15;
stop_name count(stop_code)
---------- ----------------
ZOB Ost 9
Staufenrin 9
Universit 8
Donaustadi 6
Egertweg 6
Römerplat 6
Gewerbesch 6
ZUP 6
Stadtwerke 5
Eselsberg 5
Sonnenstra 5
Theodor-He 5
Kuhberg Sc 5
Hauptbahnh 4
Theater 4

Furthermore, some stop points (i.e., the platforms within one stop) are assigned doubly or even thrice with the same coordinates. This happens if one stop point is being served by different operational branches. In the case of the SWU feed, the internal data format distinguishes between bus, tram and night bus service. This means that if a stop point is being served by all three of those branches, it will appear thrice in the data set:

SELECT stop_id, stop_code, stop_name, stop_lat, stop_lon, COUNT(distinct stop_lon) FROM stops GROUP BY stop_code HAVING count(distinct stop_lon) > 1 ORDER BY count(distinct stop_lon) DESC limit 20;
stop_id stop_code stop_name stop_lat stop_lon COUNT(distinct stop_lon)
---------- ---------- ----------------- ---------- ---------- ------------------------
3579 1240 Universität Süd 48.421839 9.956403 6
3559 1383 Gewerbeschulen K� 48.384625 9.959908 6
1869 1700 ZUP 48.39179 10.0032 6
117 1050 Staufenring 48.403521 10.004344 5
3745 1200 Eselsberg Hasenko 48.414326 9.962903 5
3557 1390 Kuhberg Schulzent 48.383775 9.955289 5
1235 1052 Donaustadion 48.405101 10.006927 4
145 1072 Mecklenburgweg 48.43453 10.024493 4
147 1073 Thüringenweg 48.433379 10.020177 4
149 1074 Haslacher Weg 48.429919 10.013786 4
3260 1087 Egertweg 48.42583 10.01243 4
217 1171 Manfred-Börner-S 48.41967 9.942766 4
3581 1241 Botanischer Garte 48.424912 9.956829 4
3584 1245 Kliniken Wissensc 48.424331 9.952453 4
3586 1246 Universität West 48.422234 9.947201 4
3565 1360 Römerplatz 48.39077 9.975428 4
3561 1393 Grimmelfinger Weg 48.38564 9.965312 4
774 1506 Benzstraße 48.365706 9.941817 4
75 1008 Hauptbahnhof 48.39983 9.98404 3
3571 1020 Stadtwerke 48.4031 9.986038 3

I hacked together the following python script that will unify these stop points so that each coordinate will appear exactly once. That is, it will output SQL statements to be pasted into sqlite which should do the trick. Hacky and crappy, but there we go. Note that I have uncommented (and did not test) the transfers bit, since the SWU feed does not use transfers. Note that this leaves the multiply assigned stop_ids untreated, as of now.

sched = pygtfs.Schedule("gtfs.db")
sq = sched.stops_query
d = {}
for each in sq.all():
if each.stop_code in d:
for existing in d[each.stop_code]:
if (d[each.stop_code][existing].stop_lat == each.stop_lat) and (d[each.stop_code][existing].stop_lon == each.stop_lon):
print('UPDATE stop_times SET stop_id = ' + d[each.stop_code][existing].stop_id + ' WHERE stop_id = ' + each.stop_id + ';')
#print('UPDATE transfers SET from_stop_id = '+ d[each.stop_code][existing].stop_id + ' WHERE from_stop_id = ' + each.stop_id + ';')
#print('UPDATE transfers SET to_stop_id = '+ d[each.stop_code][existing].stop_id + ' WHERE to_stop_id = ' + each.stop_id + ';')
print('DELETE FROM stops WHERE stop_id = ' + each.stop_id + ';')
d[each.stop_code][each.stop_id] = each;
else:
d[each.stop_code] = {};
d[each.stop_code][each.stop_id] = each

Step 3: Removing Deadheads

One bug in the current SWU feed is that it includes trips that should not be facing towards the customer. Namely, all the trips between the bus depot and the first stop on one vehicle’s Umlauf, and the return trips towards the depot. This might result in strange routing results and will definitely make the transitfeed validator complain. The following SQL statements will delete both the stop_times and the trips for those deadheads:

DELETE FROM trips WHERE trip_id IN (SELECT DISTINCT trip_id FROM stop_times WHERE stop_id IN ("170", "171"));
DELETE FROM stop_times WHERE trip_id IN (SELECT DISTINCT trip_id FROM stop_times WHERE stop_id IN ("170", "171");

Step 4: Re-Exporting the data, casting times past 24:00:00 without using strftime

This was one part which kept me scratching my head for way too long. Simple full-table dumps are not a big issue:

sqlite3 -header -csv gtfs.db 'SELECT * FROM trips'  > trips.txt

After some successfull exports, I hit a bump in the road with the stop_times table because of the way GTFS and sqlite each store times. A vehicle departing one minute before midnight from one stop and arriving two minutes past midnight at the next stop will be modeled with the textual entries of „23:59:00“ and „24:02:00“, respectively. Since the operational day ends somewhen between midnight and the wee hours of the next day, it is not unusual to encounter departure/arrival times that read something like „27:35:00“ – at 03:35 a.m. of the following day, but still within the validity period of the previous/current day (I am getting mixed up with terminology myself, I guess)

However, pygtfs will convert all those textual time entries into DateTime objects, which means that the two times above would be stored as 1970-01-01 23:59:00.000000 and 1970-01-02 00:02:00.000000. And any manipulation with strftime and other functions to manipulate Date and Time didn’t get me anywhere – or, to put it differently, all the usual functions correctly and stubbornly refused to return non-standard times that use funny hours like 24 or anything above.

In the end, the statement to get GTFS compliant times was not that hard, after all. If you ever want to calculate hours, minutes and seconds past a date, this is the way to go:

SELECT trip_id, printf("%02d",((strftime("%s", "arrival_time") - strftime("%s", "1970-01-01 00:00:00.00000")) / (60*60))) || ":" || printf("%02d",(((strftime("%s", "arrival_time") - strftime("%s", "1970-01-01 00:00:00.00000")) % (60*60)) / 60)) || ":" || printf("%02d",(((strftime("%s", "arrival_time") - strftime("%s", "1970-01-01 00:00:00.00000")) % 60) % 60)) AS arrival_time, printf("%02d",((strftime("%s", "departure_time") - strftime("%s", "1970-01-01 00:00:00.00000")) / (60*60))) || ":" || printf("%02d",(((strftime("%s", "departure_time") - strftime("%s", "1970-01-01 00:00:00.00000")) % (60*60)) / 60)) || ":" || printf("%02d",(((strftime("%s", "departure_time") - strftime("%s", "1970-01-01 00:00:00.00000")) % 60) % 60)) AS departure_time, stop_id, stop_sequence, stop_headsign, pickup_type, drop_off_type, shape_dist_traveled, timepoint FROM stop_times

But, as @LucasWerkmeistr pointed out, in this case we are always subtracting 1970-01-01 00:00:00.00000 – which is zero. The simplified version for exporting the tables is as follows:

sqlite3 -header -csv gtfs.db 'SELECT trip_id, printf("%02d",(strftime("%s", "arrival_time") / (60*60))) || ":" || printf("%02d",(strftime("%s", "arrival_time") % (60*60)) / 60) || ":" || printf("%02d",(strftime("%s", "arrival_time") % 60) % 60) AS arrival_time, printf("%02d",(strftime("%s", "departure_time")) / (60*60)) || ":" || printf("%02d",(strftime("%s", "departure_time")  % (60*60)) / 60) || ":" || printf("%02d",(strftime("%s", "departure_time") % 60) % 60) AS departure_time, stop_id, stop_sequence, stop_headsign, pickup_type, drop_off_type, shape_dist_traveled, timepoint FROM stop_times'  > stop_times.txt

Note that the export mentions the shape_dist_traveled and timepoint columns. This is because this iteration was not made on a squeaky clean SWU feed, but after some massaging with Conveyal’s GTFS Editor. But that will be a topic for another time 😉

edited on 2019-03-16 to fix errors in the last two scripts and to clarify stuff

Mal grafisch aufbereitet: Der Status Quo zu Open Data im OPNV

Dreieinhalb Jahre ist es jetzt her, dass die kleinen SWU den bundesweit zweiten GTFS-Fahrplandatensatz veroeffentlichten. Und was sind wir in der Zwischenzeit alles rumgeturnt durch Deutschland, um Bruecken zu schlagen zwischen Civic-Tech-Szene und Verkehrsunternehmen: Auf der rp15, beim Verkehrscamp des VDV in Essen, und im letzten Jahr auch immer wieder bei der Bahn.

Hinter den Kulissen bewegt sich in der Tat sehr viel, was mich dieses Jahr auf der re:publica auch verhalten optimistisch gestimmt hat. Vorne ist davon aber nur maessig viel zu sehen. Ein aktueller Realitaetscheck: Die Verbundkarte, die ich auf dem letzten DB-Hackathon angefangen hatte, aber nun deutlich besser von Alexey umgesetzt wurde:

Ich wuerde sagen: Da geht noch was.

Lebenszeichen

Wer hier mitliest, koennte meinen, ich existiere gar nicht mehr – ich kann wahlweise beruhigen oder enttaeuschen, dem ist nicht so.

Die letzten Monate waren ein wenig turbulent: Diplomarbeitsendspurt und -abschlussvortrag, nahtloser Uebergang in SoNaFe-Hilfsorganisatortaetigkeit wider Willen, parallel Arbeit im Mobilitaetsreferat, der datalove-Arbeitsgruppe, ein Open-Data-Workshop beim SWR in Stuttgart… was man eben so tut 😉

Die kommenden Wochen bleiben voller spannender Projekte: Ab Freitag abend bin ich in Berlin zum offiziellen Start von Code for Germany, ausserdem habe ich einige Texte zu Open Data und Nahverkehr auf dem Backburner, und Dominik und ich prototypen gerade Libraries fuer neue Busabfahrtsdisplays auf GTFS-Basis. Da Farnell element 14 uns hier freundlicherweise mit Hardware unterstuetzt, gibt’s bald auch eine Variante zum einfach-nur-installieren-und-verwenden – aber erstmal klinke ich mich nun in Richtung Berlin aus 🙂

Bis dahin ein Sneak Peak:

Roll your own transit display

@Lotterleben pointed out a hackaday project by Karlsruhe students today: Upcycling an old LED dot-matrix display, they outfitted their dorm with a real time bus departure monitor. Of course, there is a German word for that: Dynamische Fahrgastinformationsanzeige, or DFI 🙂

Apart from the cool technical solution the two came up with, this monitor is also interesting from yet another perspective. KVV, Karlsruhe’s integrated transit system, uses EFA by mentzDV for their online journey planner, which is in turn parsed for the display hack. EFA can output XML or JSON (depending on the installation), and the datalove working group used the EFA XML output for their own transit displays at ulm university (see below) – or for your own desktop, or for your smartphone.

uni-forum

 

Now, neither the departure monitor, nor the smartphone version, are works of art. They have severe usability and UI issues, and the EFA wrapper, while a cool hack by @taxilof, is tailored towards DING, our integrated transit system. Also, all of the systems completely rely on EFA – thus, whenever EFA fails or has scheduled downtimes, all displays fail.

I would love to bring together all interested parties who have hitherto put some efforts into any wrappers, libraries or solutions for EFA and create a unified and good library for EFA that is interchangeable to whatever version your local authority is running – just insert the API endpoint, choose whether it can output JSON or XML, and be a happy camper. Bonus points for also integrating GTFS as a fallback solution[1]!

The code for the mobile departure site is on Github; Documentation on the EFA interface is collected in the UlmAPI wiki. Interested? Get in touch!

[1] Yes, your transit system most likely does not provide GTFS yet. I am working on this as part of my thesis.

Update: @Natanji and @Feuerrot pointed me towards the projects site of Daniel Friesel, which includes command-line interfaces to – among others – EFA and Deutsche Bahn departure monitors. Front ends are available in the style of Deutsche Bahn and VRR.

Update2: There is another site serving as a entry point into creating your own Deutsche Bahn departure monitors.

Langsam laeufts an

Seit eineinhalb Wochen gibt es nun offiziell die Soll-Fahrplandaten von den Stadtwerken unter freier Lizenz, und langsam faengt auch die Adaption und Integration in anderer Leute Werkzeuge an. Stefan Wehrmeyer hatte ja noch am Abend des VBB-EntwicklerInnen-Nachtreffens den Datensatz beantragt und Mapnificent entsprechend erweitert; mittlerweile hat auch Knut aus Berlin sein Werkzeug umgebaut, mit dem er die Haltepunkte aus dem GTFS-Feed mit den Haltepunkten in der OpenStreetMap vergleicht. Damit einhergehend kam auch gleich der erste Bug Report zum Feed, den ich heute mittag auch gleich loesen koennte — der bisherige Transformationswerkzeugkasten ist ein wenig codierungsagnostisch, so dass versehentlich UTF-8-codierte Zeichen in ASCII-Textdateien landeten.

Ein weiteres kleines Werkzeug habe ich aus der Ergebnispraesentation des VBB-EntwicklerInnen-Nachtreffens mitgenommen, das auch schnell auf Ulm adaptiert war: Analog zu Mapnificent eine Erreichbarkeitskarte, die Kreise mit waehlbarem Radius um Haltepunkte zeichnet und so anzeigt, wie „erreichbar“ der Nahverkehr in der Stadt ist. 800 Meter (der vorgegebene Wert aus Berlin) umfassen gleich quasi ganz Ulm, interessanter waere da die Abbildung des ganzen DING-Verbunds — die Daten hab ich zwar schon lange, aber leider nicht unter freier Lizenz 😉

Ein weiterer Kritikpunkt sowohl von Mapnificent als auch dieser Erreichbarkeitskarte ist ihre… begrenzte Aussagefaehigkeit (um den Begriff „Unsinnigkeit“ zu vermeiden :D). Konzentrische Kreise bilden quasi nie die Gehdistanz von/zu einem Haltepunkt ab. In einer typischen US-amerikanischen Planstadt waeren Rauten passender, in typischen europaeischen Staedten sieht es meist von Haltepunkt zu Haltepunkt anders aus. Hier mal eine Karte mit tatsaechlichem Fussgang-Routing zu bauen, das waer mal was 😉