Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/import/gtfs/gtfs2fcd.py
169679 views
1
#!/usr/bin/env python3
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2010-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file gtfs2fcd.py
15
# @author Michael Behrisch
16
# @author Robert Hilbrich
17
# @author Mirko Barthauer
18
# @date 2018-06-13
19
20
"""
21
Converts GTFS data into separate fcd traces for every distinct trip
22
"""
23
24
from __future__ import print_function
25
from __future__ import absolute_import
26
import os
27
import sys
28
import io
29
import pandas as pd
30
import zipfile
31
32
sys.path.append(os.path.join(os.environ["SUMO_HOME"], "tools"))
33
import sumolib # noqa
34
from sumolib.miscutils import humanReadableTime # noqa
35
import traceExporter # noqa
36
import gtfs2osm # noqa
37
38
39
def add_options():
40
op = sumolib.options.ArgumentParser(
41
description="converts GTFS data into separate fcd traces for every distinct trip")
42
op.add_argument("-r", "--region", default="gtfs", category="input",
43
help="define the region to process")
44
gp = op.add_mutually_exclusive_group(required=True)
45
gp.add_argument("--gtfs", category="input", type=op.data_file, fix_path=True,
46
help="define gtfs zip file to load (mandatory)")
47
gp.add_argument("--merged-csv", category="input", type=op.data_file, dest="mergedCSV", fix_path=True,
48
help="define csv file for loading merged data (instead of gtfs data)")
49
op.add_argument("--merged-csv-output", category="output", type=op.data_file, dest="mergedCSVOutput",
50
help="define csv file for saving merged GTFS data")
51
op.add_argument("--date", category="input", required=False, help="define the day to import, format: 'YYYYMMDD'")
52
op.add_argument("--fcd", category="input", type=op.data_file,
53
help="directory to write / read the generated FCD files to / from")
54
op.add_argument("--gpsdat", category="input", type=op.data_file,
55
help="directory to write / read the generated gpsdat files to / from")
56
op.add_argument("--modes", category="input", help="comma separated list of modes to import (%s)" %
57
(", ".join(gtfs2osm.OSM2SUMO_MODES.keys())))
58
op.add_argument("--vtype-output", default="vtypes.xml", category="output", type=op.file,
59
help="file to write the generated vehicle types to")
60
op.add_argument("--write-terminals", action="store_true", default=False,
61
dest="writeTerminals", category="processing",
62
help="Write vehicle parameters that describe terminal stops and times")
63
op.add_argument("-H", "--human-readable-time", category="output", dest="hrtime", default=False, action="store_true",
64
help="write times as h:m:s")
65
op.add_argument("-v", "--verbose", action="store_true", default=False,
66
category="processing", help="tell me what you are doing")
67
op.add_argument("-b", "--begin", default=0, category="time", type=op.time,
68
help="Defines the begin time to export")
69
op.add_argument("-e", "--end", default=86400, category="time", type=op.time,
70
help="Defines the end time for the export")
71
op.add_argument("--bbox", category="input", help="define the bounding box to filter the gtfs data, format: W,S,E,N")
72
return op
73
74
75
def check_options(options):
76
if options.fcd is None:
77
options.fcd = os.path.join('fcd', options.region)
78
if options.gpsdat is None:
79
options.gpsdat = os.path.join('input', options.region)
80
if options.modes is None:
81
options.modes = ",".join(gtfs2osm.OSM2SUMO_MODES.keys())
82
if options.gtfs and not options.date:
83
raise ValueError("When option --gtfs is set, option --date must be set as well")
84
85
return options
86
87
88
def time2sec(s):
89
t = s.split(":")
90
return int(t[0]) * 3600 + int(t[1]) * 60 + int(t[2])
91
92
93
def get_merged_data(options):
94
gtfsZip = zipfile.ZipFile(sumolib.openz(options.gtfs, mode="rb", tryGZip=False, printErrors=True))
95
routes, trips_on_day, shapes, stops, stop_times = gtfs2osm.import_gtfs(options, gtfsZip)
96
gtfsZip.fp.close()
97
98
if options.bbox:
99
stops['stop_lat'] = stops['stop_lat'].astype(float)
100
stops['stop_lon'] = stops['stop_lon'].astype(float)
101
stops = stops[(options.bbox[1] <= stops['stop_lat']) & (stops['stop_lat'] <= options.bbox[3]) &
102
(options.bbox[0] <= stops['stop_lon']) & (stops['stop_lon'] <= options.bbox[2])]
103
stop_times['arrival_time'] = stop_times['arrival_time'].map(time2sec)
104
stop_times['departure_time'] = stop_times['departure_time'].map(time2sec)
105
106
if 'fare_stops.txt' in gtfsZip.namelist():
107
zones = pd.read_csv(gtfsZip.open('fare_stops.txt'), dtype=str)
108
stops_merged = pd.merge(pd.merge(stops, stop_times, on='stop_id'), zones, on='stop_id')
109
else:
110
stops_merged = pd.merge(stops, stop_times, on='stop_id')
111
stops_merged['fare_zone'] = ''
112
stops_merged['fare_token'] = ''
113
stops_merged['start_char'] = ''
114
115
trips_routes_merged = pd.merge(trips_on_day, routes, on='route_id')
116
merged = pd.merge(stops_merged, trips_routes_merged,
117
on='trip_id')[['trip_id', 'route_id', 'route_short_name', 'route_type',
118
'stop_id', 'stop_name', 'stop_lat', 'stop_lon', 'stop_sequence',
119
'fare_zone', 'fare_token', 'start_char', 'trip_headsign',
120
'arrival_time', 'departure_time']].drop_duplicates()
121
return merged
122
123
124
def dataAvailable(options):
125
for mode in options.modes.split(","):
126
if os.path.exists(os.path.join(options.fcd, "%s.fcd.xml" % mode)):
127
return True
128
return False
129
130
131
def main(options):
132
ft = humanReadableTime if options.hrtime else lambda x: x
133
if options.mergedCSV:
134
full_data_merged = pd.read_csv(options.mergedCSV, sep=";",
135
keep_default_na=False,
136
dtype={"route_type": str})
137
else:
138
full_data_merged = get_merged_data(options)
139
if options.mergedCSVOutput:
140
full_data_merged.sort_values(by=['trip_id', 'stop_sequence'], inplace=True)
141
full_data_merged.to_csv(options.mergedCSVOutput, sep=";", index=False)
142
if full_data_merged.empty:
143
return False
144
fcdFile = {}
145
tripFile = {}
146
if not os.path.exists(options.fcd):
147
os.makedirs(options.fcd)
148
seenModes = set()
149
modes = set(options.modes.split(","))
150
for mode in modes:
151
filePrefix = os.path.join(options.fcd, mode)
152
fcdFile[mode] = io.open(filePrefix + '.fcd.xml', 'w', encoding="utf8")
153
sumolib.writeXMLHeader(fcdFile[mode], "gtfs2fcd.py", options=options)
154
fcdFile[mode].write(u'<fcd-export>\n')
155
if options.verbose:
156
print('Writing fcd file "%s"' % fcdFile[mode].name)
157
tripFile[mode] = io.open(filePrefix + '.rou.xml', 'w', encoding="utf8")
158
tripFile[mode].write(u"<routes>\n")
159
timeIndex = 0
160
for _, trip_data in full_data_merged.groupby('route_id'):
161
seqs = {}
162
for trip_id, data in trip_data.groupby('trip_id'):
163
stopSeq = []
164
buf = u""
165
offset = 0
166
firstDep = None
167
firstStop = None
168
lastIndex = None
169
lastArrival = None
170
lastStop = None
171
for idx, d in data.sort_values(by=['stop_sequence']).iterrows():
172
if d.stop_sequence == lastIndex:
173
print("Invalid stop_sequence in input for trip %s" % trip_id, file=sys.stderr)
174
if lastArrival is not None:
175
if d.arrival_time < lastArrival:
176
print("Warning! Stop %s for vehicle %s starts earlier (%s) than previous stop (%s)" % (
177
idx, trip_id, d.arrival_time, lastArrival), file=sys.stderr)
178
lastArrival = d.arrival_time
179
lastStop = d.stop_name
180
181
arrivalSec = d.arrival_time + timeIndex
182
stopSeq.append(d.stop_id)
183
departureSec = d.departure_time + timeIndex
184
until = 0 if firstDep is None else departureSec - timeIndex - firstDep
185
buf += ((u' <timestep time="%s"><vehicle id="%s" x="%s" y="%s" until="%s" ' +
186
u'name=%s fareZone="%s" fareSymbol="%s" startFare="%s" speed="20"/></timestep>\n') %
187
(arrivalSec - offset, trip_id, d.stop_lon, d.stop_lat, until,
188
sumolib.xml.quoteattr(d.stop_name, True), d.fare_zone, d.fare_token, d.start_char))
189
if firstDep is None:
190
firstDep = departureSec - timeIndex
191
firstStop = d.stop_name
192
offset += departureSec - arrivalSec
193
lastIndex = d.stop_sequence
194
mode = gtfs2osm.GTFS2OSM_MODES[d.route_type]
195
if mode in modes:
196
s = tuple(stopSeq)
197
if s not in seqs:
198
seqs[s] = trip_id
199
fcdFile[mode].write(buf)
200
timeIndex = arrivalSec
201
tripFile[mode].write(u' <vehicle id="%s" route="%s" type="%s" depart="%s" line="%s">\n' %
202
(trip_id, seqs[s], mode, firstDep, seqs[s]))
203
params = [("gtfs.route_name", d.route_short_name)]
204
if d.trip_headsign:
205
params.append(("gtfs.trip_headsign", d.trip_headsign))
206
if options.writeTerminals:
207
params += [("gtfs.origin_stop", firstStop),
208
("gtfs.origin_depart", ft(firstDep)),
209
("gtfs.destination_stop", lastStop),
210
("gtfs.destination_arrrival", ft(lastArrival))]
211
for k, v in params:
212
tripFile[mode].write(u' <param key="%s" value=%s/>\n' % (
213
k, sumolib.xml.quoteattr(str(v), True)))
214
tripFile[mode].write(u' </vehicle>\n')
215
seenModes.add(mode)
216
for mode in modes:
217
fcdFile[mode].write(u'</fcd-export>\n')
218
fcdFile[mode].close()
219
tripFile[mode].write(u"</routes>\n")
220
tripFile[mode].close()
221
if mode not in seenModes:
222
os.remove(fcdFile[mode].name)
223
os.remove(tripFile[mode].name)
224
if options.gpsdat:
225
if not os.path.exists(options.gpsdat):
226
os.makedirs(options.gpsdat)
227
for mode in modes:
228
if mode in seenModes:
229
traceExporter.main(['--base-date', '0', '-i', fcdFile[mode].name,
230
'--gpsdat-output', os.path.join(options.gpsdat, "gpsdat_%s.csv" % mode)])
231
if dataAvailable(options):
232
gtfs2osm.write_vtypes(options, seenModes)
233
return True
234
235
236
if __name__ == "__main__":
237
main(check_options(add_options().parse_args()))
238
239