gtfs-parser 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gtfs_parser/__init__.py +2 -0
- gtfs_parser/__main__.py +128 -0
- gtfs_parser/aggregate.py +452 -0
- gtfs_parser/gtfs.py +32 -0
- gtfs_parser/parse.py +178 -0
- gtfs_parser-0.1.0.dist-info/LICENSE +7 -0
- gtfs_parser-0.1.0.dist-info/METADATA +67 -0
- gtfs_parser-0.1.0.dist-info/RECORD +10 -0
- gtfs_parser-0.1.0.dist-info/WHEEL +4 -0
- gtfs_parser-0.1.0.dist-info/entry_points.txt +3 -0
gtfs_parser/__init__.py
ADDED
gtfs_parser/__main__.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import zipfile
|
|
4
|
+
import tempfile
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
from .gtfs import GTFS
|
|
9
|
+
from .parse import read_routes, read_stops
|
|
10
|
+
from .aggregate import Aggregator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_args():
|
|
14
|
+
parser = argparse.ArgumentParser()
|
|
15
|
+
parser.add_argument("mode")
|
|
16
|
+
parser.add_argument("src")
|
|
17
|
+
parser.add_argument("dst")
|
|
18
|
+
parser.add_argument("--parse_ignoreshapes", action="store_true")
|
|
19
|
+
parser.add_argument("--parse_ignorenoroute", action="store_true")
|
|
20
|
+
parser.add_argument("--aggregate_yyyymmdd")
|
|
21
|
+
parser.add_argument("--aggregate_nounifystops", action="store_true")
|
|
22
|
+
parser.add_argument("--aggregate_delimiter")
|
|
23
|
+
parser.add_argument("--aggregate_begintime")
|
|
24
|
+
parser.add_argument("--aggregate_endtime")
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
return args
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_args(args):
|
|
30
|
+
if args.aggregate_yyyymmdd:
|
|
31
|
+
if len(args.aggregate_yyyymmdd) != 8:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
f"yyyymmdd must be 8 characters string, for example 20210401, \
|
|
34
|
+
your is {args.aggregate_yyyymmdd} ({len(args.aggregate_yyyymmdd)} characters)"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if args.aggregate_begintime:
|
|
38
|
+
if len(args.aggregate_begintime) != 6:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
f'begintime must be "hhmmss", your is {args.aggregate_begintime}'
|
|
41
|
+
)
|
|
42
|
+
if not args.aggregate_endtime:
|
|
43
|
+
raise RuntimeError("endtime is not set.")
|
|
44
|
+
|
|
45
|
+
if args.aggregate_endtime:
|
|
46
|
+
if len(args.aggregate_endtime) != 6:
|
|
47
|
+
raise RuntimeError(
|
|
48
|
+
f'endtime must be "hhmmss", your is {args.aggregate_endtime}'
|
|
49
|
+
)
|
|
50
|
+
if not args.aggregate_begintime:
|
|
51
|
+
raise RuntimeError("begintime is not set.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
args = load_args()
|
|
56
|
+
validate_args(args)
|
|
57
|
+
|
|
58
|
+
if args.src.endswith(".zip"): # TODO: wiser checking
|
|
59
|
+
print("extracting zipfile...")
|
|
60
|
+
temp_dir = os.path.join(tempfile.gettempdir(), "gtfs_parser")
|
|
61
|
+
if os.path.exists(temp_dir):
|
|
62
|
+
shutil.rmtree(temp_dir)
|
|
63
|
+
os.mkdir(temp_dir)
|
|
64
|
+
with zipfile.ZipFile(args.zip) as z:
|
|
65
|
+
z.extractall(temp_dir)
|
|
66
|
+
output_dir = temp_dir
|
|
67
|
+
else:
|
|
68
|
+
output_dir = args.src
|
|
69
|
+
|
|
70
|
+
gtfs = GTFS(output_dir)
|
|
71
|
+
print("GTFS loaded.")
|
|
72
|
+
|
|
73
|
+
os.makedirs(args.dst, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
if args.mode == "aggregate":
|
|
76
|
+
aggregator = Aggregator(
|
|
77
|
+
gtfs,
|
|
78
|
+
no_unify_stops=args.aggregate_nounifystops,
|
|
79
|
+
delimiter=args.aggregate_delimiter,
|
|
80
|
+
yyyymmdd=args.aggregate_yyyymmdd,
|
|
81
|
+
begin_time=args.aggregate_begintime,
|
|
82
|
+
end_time=args.aggregate_endtime,
|
|
83
|
+
)
|
|
84
|
+
aggregated_routes_geojson = {
|
|
85
|
+
"type": "FeatureCollection",
|
|
86
|
+
"features": aggregator.read_route_frequency(),
|
|
87
|
+
}
|
|
88
|
+
aggregated_stops_geojson = {
|
|
89
|
+
"type": "FeatureCollection",
|
|
90
|
+
"features": aggregator.read_interpolated_stops(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
with open(
|
|
94
|
+
os.path.join(args.dst, "aggregated_routes.geojson"),
|
|
95
|
+
mode="w",
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
) as f:
|
|
98
|
+
json.dump(aggregated_routes_geojson, f, ensure_ascii=False)
|
|
99
|
+
with open(
|
|
100
|
+
os.path.join(args.dst, "aggregated_stops.geojson"),
|
|
101
|
+
mode="w",
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
) as f:
|
|
104
|
+
json.dump(aggregated_stops_geojson, f, ensure_ascii=False)
|
|
105
|
+
elif args.mode == "parse":
|
|
106
|
+
routes_geojson = {
|
|
107
|
+
"type": "FeatureCollection",
|
|
108
|
+
"features": read_routes(gtfs, ignore_shapes=args.parse_ignoreshapes),
|
|
109
|
+
}
|
|
110
|
+
stops_geojson = {
|
|
111
|
+
"type": "FeatureCollection",
|
|
112
|
+
"features": read_stops(gtfs, ignore_no_route=args.parse_ignorenoroute),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
with open(
|
|
116
|
+
os.path.join(args.dst, "routes.geojson"), mode="w", encoding="utf-8"
|
|
117
|
+
) as f:
|
|
118
|
+
json.dump(routes_geojson, f, ensure_ascii=False)
|
|
119
|
+
with open(
|
|
120
|
+
os.path.join(args.dst, "stops.geojson"), mode="w", encoding="utf-8"
|
|
121
|
+
) as f:
|
|
122
|
+
json.dump(stops_geojson, f, ensure_ascii=False)
|
|
123
|
+
else:
|
|
124
|
+
raise RuntimeError("mode must be 'parse' or 'aggregate")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
gtfs_parser/aggregate.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
import datetime
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def latlon_to_str(latlon):
|
|
8
|
+
return "".join(list(map(lambda coord: str(round(coord, 4)), latlon)))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Aggregator:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
gtfs: dict,
|
|
15
|
+
no_unify_stops=False,
|
|
16
|
+
delimiter="",
|
|
17
|
+
max_distance_degree=0.01,
|
|
18
|
+
yyyymmdd="",
|
|
19
|
+
begin_time="",
|
|
20
|
+
end_time="",
|
|
21
|
+
):
|
|
22
|
+
self.gtfs = gtfs
|
|
23
|
+
self.similar_stops_df = None
|
|
24
|
+
|
|
25
|
+
self.__aggregate_similar_stops(
|
|
26
|
+
delimiter,
|
|
27
|
+
max_distance_degree,
|
|
28
|
+
no_unify_stops,
|
|
29
|
+
yyyymmdd=yyyymmdd,
|
|
30
|
+
begin_time=begin_time,
|
|
31
|
+
end_time=end_time,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __aggregate_similar_stops(
|
|
35
|
+
self,
|
|
36
|
+
delimiter: str,
|
|
37
|
+
max_distance_degree: float,
|
|
38
|
+
no_unify_stops: bool,
|
|
39
|
+
yyyymmdd="",
|
|
40
|
+
begin_time="",
|
|
41
|
+
end_time="",
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
this method occurs side-effect to modify self.gtfs and self.similar_stops_df
|
|
45
|
+
"""
|
|
46
|
+
# filter stop_times by whether serviced or not
|
|
47
|
+
if yyyymmdd:
|
|
48
|
+
trips_filtered_by_day = self.__get_trips_on_a_date(yyyymmdd)
|
|
49
|
+
self.gtfs["stop_times"] = pd.merge(
|
|
50
|
+
self.gtfs["stop_times"],
|
|
51
|
+
trips_filtered_by_day,
|
|
52
|
+
on="trip_id",
|
|
53
|
+
how="left",
|
|
54
|
+
)
|
|
55
|
+
self.gtfs["stop_times"] = self.gtfs["stop_times"][
|
|
56
|
+
self.gtfs["stop_times"]["service_flag"] == 1
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# time filter
|
|
60
|
+
if begin_time and end_time:
|
|
61
|
+
# departure_time is nullable and expressed in "hh:mm:ss" or "h:mm:ss" format.
|
|
62
|
+
# Hour can be mor than 24.
|
|
63
|
+
# Therefore, drop null records and convert times to integers.
|
|
64
|
+
int_dep_times = (
|
|
65
|
+
self.gtfs["stop_times"].departure_time.str.replace(":", "").astype(int)
|
|
66
|
+
)
|
|
67
|
+
self.gtfs["stop_times"] = self.gtfs["stop_times"][
|
|
68
|
+
self.gtfs["stop_times"].departure_time != ""
|
|
69
|
+
][(int_dep_times >= int(begin_time)) & (int_dep_times < int(end_time))]
|
|
70
|
+
|
|
71
|
+
if no_unify_stops:
|
|
72
|
+
# no unifying stops
|
|
73
|
+
self.gtfs["stops"]["similar_stop_id"] = self.gtfs["stops"]["stop_id"]
|
|
74
|
+
self.gtfs["stops"]["similar_stop_name"] = self.gtfs["stops"]["stop_name"]
|
|
75
|
+
self.gtfs["stops"]["similar_stops_centroid"] = self.gtfs["stops"][
|
|
76
|
+
["stop_lon", "stop_lat"]
|
|
77
|
+
].values.tolist()
|
|
78
|
+
self.gtfs["stops"]["position_count"] = 1
|
|
79
|
+
self.similar_stops_df = self.gtfs["stops"][
|
|
80
|
+
[
|
|
81
|
+
"similar_stop_id",
|
|
82
|
+
"similar_stop_name",
|
|
83
|
+
"similar_stops_centroid",
|
|
84
|
+
"position_count",
|
|
85
|
+
]
|
|
86
|
+
].copy()
|
|
87
|
+
else:
|
|
88
|
+
parent_ids = self.gtfs["stops"]["parent_station"].unique()
|
|
89
|
+
self.gtfs["stops"]["is_parent"] = self.gtfs["stops"]["stop_id"].map(
|
|
90
|
+
lambda stop_id: 1 if stop_id in parent_ids else 0
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.gtfs["stops"][
|
|
94
|
+
["similar_stop_id", "similar_stop_name", "similar_stops_centroid"]
|
|
95
|
+
] = (
|
|
96
|
+
self.gtfs["stops"]["stop_id"]
|
|
97
|
+
.map(
|
|
98
|
+
lambda stop_id: self.__get_similar_stop_tuple(
|
|
99
|
+
stop_id, delimiter, max_distance_degree
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
.apply(pd.Series)
|
|
103
|
+
)
|
|
104
|
+
self.gtfs["stops"]["position_id"] = self.gtfs["stops"][
|
|
105
|
+
"similar_stops_centroid"
|
|
106
|
+
].map(latlon_to_str)
|
|
107
|
+
self.gtfs["stops"]["unique_id"] = (
|
|
108
|
+
self.gtfs["stops"]["similar_stop_id"]
|
|
109
|
+
+ self.gtfs["stops"]["position_id"]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# sometimes stop_name accidently becomes pd.Series instead of str.
|
|
113
|
+
self.gtfs["stops"]["similar_stop_name"] = self.gtfs["stops"][
|
|
114
|
+
"similar_stop_name"
|
|
115
|
+
].map(lambda val: val if type(val) == str else val.stop_name)
|
|
116
|
+
|
|
117
|
+
position_count = (
|
|
118
|
+
self.gtfs["stop_times"]
|
|
119
|
+
.merge(self.gtfs["stops"], on="stop_id", how="left")
|
|
120
|
+
.groupby("position_id")
|
|
121
|
+
.size()
|
|
122
|
+
.to_frame()
|
|
123
|
+
.reset_index()
|
|
124
|
+
)
|
|
125
|
+
position_count.columns = ["position_id", "position_count"]
|
|
126
|
+
|
|
127
|
+
self.similar_stops_df = pd.merge(
|
|
128
|
+
self.gtfs["stops"].drop_duplicates(subset="position_id")[
|
|
129
|
+
[
|
|
130
|
+
"position_id",
|
|
131
|
+
"similar_stop_id",
|
|
132
|
+
"similar_stop_name",
|
|
133
|
+
"similar_stops_centroid",
|
|
134
|
+
]
|
|
135
|
+
],
|
|
136
|
+
position_count,
|
|
137
|
+
on="position_id",
|
|
138
|
+
how="left",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@lru_cache(maxsize=None)
|
|
142
|
+
def __get_similar_stop_tuple(
|
|
143
|
+
self, stop_id: str, delimiter="", max_distance_degree=0.01
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
With one stop_id, group stops by parent, stop_id, or stop_name and each distance.
|
|
147
|
+
- parent: if stop has parent_station, the 'centroid' is parent_station lat-lon
|
|
148
|
+
- stop_id: by delimiter seperate stop_id into prefix and suffix, and group stops having same stop_id-prefix
|
|
149
|
+
- name and distance: group stops by stop_name, excluding stops are far than max_distance_degree
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
stop_id (str): target stop_id
|
|
153
|
+
max_distance_degree (float, optional): distance limit on grouping, Defaults to 0.01.
|
|
154
|
+
Returns:
|
|
155
|
+
str, str, [float, float]: similar_stop_id, similar_stop_name, similar_stops_centroid
|
|
156
|
+
"""
|
|
157
|
+
stops_df = self.gtfs["stops"].sort_values("stop_id")
|
|
158
|
+
stop = stops_df[stops_df["stop_id"] == stop_id].iloc[0]
|
|
159
|
+
|
|
160
|
+
if stop["is_parent"] == 1:
|
|
161
|
+
return (
|
|
162
|
+
stop["stop_id"],
|
|
163
|
+
stop["stop_name"],
|
|
164
|
+
[stop["stop_lon"], stop["stop_lat"]],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if str(stop["parent_station"]) != "nan":
|
|
168
|
+
similar_stop_id = stop["parent_station"]
|
|
169
|
+
similar_stop = stops_df[stops_df["stop_id"] == similar_stop_id]
|
|
170
|
+
similar_stop_name = similar_stop[["stop_name"]].iloc[0]
|
|
171
|
+
similar_stop_centroid = (
|
|
172
|
+
similar_stop[["stop_lon", "stop_lat"]].iloc[0].values.tolist()
|
|
173
|
+
)
|
|
174
|
+
return similar_stop_id, similar_stop_name, similar_stop_centroid
|
|
175
|
+
|
|
176
|
+
if delimiter:
|
|
177
|
+
stops_df_id_delimited = self.__get_stops_id_delimited(delimiter)
|
|
178
|
+
stop_id_prefix = stop_id.rsplit(delimiter, 1)[0]
|
|
179
|
+
if stop_id_prefix != stop_id:
|
|
180
|
+
similar_stop_id = stop_id_prefix
|
|
181
|
+
seperated_only_stops = stops_df_id_delimited[
|
|
182
|
+
stops_df_id_delimited["delimited"]
|
|
183
|
+
]
|
|
184
|
+
similar_stops = seperated_only_stops[
|
|
185
|
+
seperated_only_stops["stop_id_prefix"] == stop_id_prefix
|
|
186
|
+
][
|
|
187
|
+
[
|
|
188
|
+
"stop_name",
|
|
189
|
+
"similar_stops_centroid_lon",
|
|
190
|
+
"similar_stops_centroid_lat",
|
|
191
|
+
]
|
|
192
|
+
]
|
|
193
|
+
similar_stop_name = similar_stops[["stop_name"]].iloc[0]
|
|
194
|
+
similar_stop_centroid = similar_stops[
|
|
195
|
+
["similar_stops_centroid_lon", "similar_stops_centroid_lat"]
|
|
196
|
+
].values.tolist()[0]
|
|
197
|
+
return similar_stop_id, similar_stop_name, similar_stop_centroid
|
|
198
|
+
else:
|
|
199
|
+
# when cannot seperate stop_id, grouping by name and distance
|
|
200
|
+
stops_df = stops_df_id_delimited[~stops_df_id_delimited["delimited"]]
|
|
201
|
+
|
|
202
|
+
# grouping by name and distance
|
|
203
|
+
similar_stops = stops_df[stops_df["stop_name"] == stop["stop_name"]][
|
|
204
|
+
["stop_id", "stop_name", "stop_lon", "stop_lat"]
|
|
205
|
+
]
|
|
206
|
+
similar_stops = similar_stops.query(
|
|
207
|
+
f'(stop_lon - {stop["stop_lon"]}) ** 2 + (stop_lat - {stop["stop_lat"]}) ** 2 < {max_distance_degree ** 2}'
|
|
208
|
+
)
|
|
209
|
+
similar_stop_centroid = (
|
|
210
|
+
similar_stops[["stop_lon", "stop_lat"]].mean().values.tolist()
|
|
211
|
+
)
|
|
212
|
+
similar_stop_id = similar_stops["stop_id"].iloc[0]
|
|
213
|
+
similar_stop_name = stop["stop_name"]
|
|
214
|
+
return similar_stop_id, similar_stop_name, similar_stop_centroid
|
|
215
|
+
|
|
216
|
+
@lru_cache(maxsize=None)
|
|
217
|
+
def __get_stops_id_delimited(self, delimiter: str):
|
|
218
|
+
stops_df = self.gtfs.get("stops")[
|
|
219
|
+
["stop_id", "stop_name", "stop_lon", "stop_lat", "parent_station"]
|
|
220
|
+
].copy()
|
|
221
|
+
stops_df["stop_id_prefix"] = stops_df["stop_id"].map(
|
|
222
|
+
lambda stop_id: stop_id.rsplit(delimiter, 1)[0]
|
|
223
|
+
)
|
|
224
|
+
stops_df["delimited"] = stops_df["stop_id"] != stops_df["stop_id_prefix"]
|
|
225
|
+
grouped_by_prefix = (
|
|
226
|
+
stops_df[["stop_id_prefix", "stop_lon", "stop_lat"]]
|
|
227
|
+
.groupby("stop_id_prefix")
|
|
228
|
+
.mean()
|
|
229
|
+
.reset_index()
|
|
230
|
+
)
|
|
231
|
+
grouped_by_prefix.columns = [
|
|
232
|
+
"stop_id_prefix",
|
|
233
|
+
"similar_stops_centroid_lon",
|
|
234
|
+
"similar_stops_centroid_lat",
|
|
235
|
+
]
|
|
236
|
+
stops_df_with_centroid = pd.merge(
|
|
237
|
+
stops_df, grouped_by_prefix, on="stop_id_prefix", how="left"
|
|
238
|
+
)
|
|
239
|
+
return stops_df_with_centroid
|
|
240
|
+
|
|
241
|
+
def read_interpolated_stops(self):
|
|
242
|
+
"""
|
|
243
|
+
Read stops "interpolated" by parent station or stop_id or stop_name and distance.
|
|
244
|
+
There are many similar stops that are near to each, has same name, or has same prefix in stop_id.
|
|
245
|
+
In traffic analyzing, it is good for that similar stops to be grouped as same stop.
|
|
246
|
+
This method group them by some elements, parent, id, name and distance.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
delimiter (str, optional): stop_id delimiter, sample_A, sample_B, then delimiter is '_'. Defaults to ''.
|
|
250
|
+
max_distance_degree (float, optional): distance limit in grouping by stop_name. Defaults to 0.01.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
[type]: [description]
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
stop_dicts = self.similar_stops_df[
|
|
257
|
+
[
|
|
258
|
+
"similar_stop_id",
|
|
259
|
+
"similar_stop_name",
|
|
260
|
+
"similar_stops_centroid",
|
|
261
|
+
"position_count",
|
|
262
|
+
]
|
|
263
|
+
].to_dict(orient="records")
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
"type": "Feature",
|
|
267
|
+
"geometry": {
|
|
268
|
+
"type": "Point",
|
|
269
|
+
"coordinates": stop["similar_stops_centroid"],
|
|
270
|
+
},
|
|
271
|
+
"properties": {
|
|
272
|
+
"similar_stop_name": stop["similar_stop_name"],
|
|
273
|
+
"similar_stop_id": stop["similar_stop_id"],
|
|
274
|
+
"count": stop["position_count"],
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
for stop in stop_dicts
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
def read_route_frequency(self):
|
|
281
|
+
"""
|
|
282
|
+
By grouped stops, aggregate route frequency.
|
|
283
|
+
Filtering trips by a date, you can aggregate frequency only route serviced on the date.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
yyyymmdd (str, optional): date, like 20210401. Defaults to ''.
|
|
287
|
+
begin_time (str, optional): 'hhmmss' <= departure time, like 030000. Defaults to ''.
|
|
288
|
+
end_time (str, optional): 'hhmmss' > departure time, like 280000. Defaults to ''.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
[type]: [description]
|
|
292
|
+
"""
|
|
293
|
+
stop_times_df = (
|
|
294
|
+
self.gtfs.get("stop_times")[
|
|
295
|
+
["stop_id", "trip_id", "stop_sequence", "departure_time"]
|
|
296
|
+
]
|
|
297
|
+
.sort_values(["trip_id", "stop_sequence"])
|
|
298
|
+
.copy()
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# join agency info)
|
|
302
|
+
stop_times_df = pd.merge(
|
|
303
|
+
stop_times_df,
|
|
304
|
+
self.gtfs["trips"][["trip_id", "route_id"]],
|
|
305
|
+
on="trip_id",
|
|
306
|
+
how="left",
|
|
307
|
+
)
|
|
308
|
+
stop_times_df = pd.merge(
|
|
309
|
+
stop_times_df,
|
|
310
|
+
self.gtfs["routes"][["route_id", "agency_id"]],
|
|
311
|
+
on="route_id",
|
|
312
|
+
how="left",
|
|
313
|
+
)
|
|
314
|
+
stop_times_df = pd.merge(
|
|
315
|
+
stop_times_df,
|
|
316
|
+
self.gtfs["agency"][["agency_id", "agency_name"]],
|
|
317
|
+
on="agency_id",
|
|
318
|
+
how="left",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# get prev and next stops_id, stop_name, trip_id
|
|
322
|
+
stop_times_df = pd.merge(
|
|
323
|
+
stop_times_df,
|
|
324
|
+
self.gtfs["stops"][
|
|
325
|
+
[
|
|
326
|
+
"stop_id",
|
|
327
|
+
"similar_stop_id",
|
|
328
|
+
"similar_stop_name",
|
|
329
|
+
"similar_stops_centroid",
|
|
330
|
+
]
|
|
331
|
+
],
|
|
332
|
+
on="stop_id",
|
|
333
|
+
how="left",
|
|
334
|
+
)
|
|
335
|
+
stop_times_df["prev_stop_id"] = stop_times_df["similar_stop_id"]
|
|
336
|
+
stop_times_df["prev_trip_id"] = stop_times_df["trip_id"]
|
|
337
|
+
stop_times_df["prev_stop_name"] = stop_times_df["similar_stop_name"]
|
|
338
|
+
stop_times_df["prev_similar_stops_centroid"] = stop_times_df[
|
|
339
|
+
"similar_stops_centroid"
|
|
340
|
+
]
|
|
341
|
+
stop_times_df["next_stop_id"] = stop_times_df["similar_stop_id"].shift(-1)
|
|
342
|
+
stop_times_df["next_trip_id"] = stop_times_df["trip_id"].shift(-1)
|
|
343
|
+
stop_times_df["next_stop_name"] = stop_times_df["similar_stop_name"].shift(-1)
|
|
344
|
+
stop_times_df["next_similar_stops_centroid"] = stop_times_df[
|
|
345
|
+
"similar_stops_centroid"
|
|
346
|
+
].shift(-1)
|
|
347
|
+
|
|
348
|
+
# drop last stops (-> stops has no next stop)
|
|
349
|
+
stop_times_df = stop_times_df.drop(
|
|
350
|
+
index=stop_times_df.query("prev_trip_id != next_trip_id").index
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# define path_id by prev-stops-centroid and next-stops-centroid
|
|
354
|
+
stop_times_df["path_id"] = (
|
|
355
|
+
stop_times_df["prev_stop_id"]
|
|
356
|
+
+ stop_times_df["next_stop_id"]
|
|
357
|
+
+ stop_times_df["prev_similar_stops_centroid"].map(latlon_to_str)
|
|
358
|
+
+ stop_times_df["next_similar_stops_centroid"].map(latlon_to_str)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# aggregate path-frequency
|
|
362
|
+
path_frequency = (
|
|
363
|
+
stop_times_df[["similar_stop_id", "path_id"]]
|
|
364
|
+
.groupby("path_id")
|
|
365
|
+
.count()
|
|
366
|
+
.reset_index()
|
|
367
|
+
)
|
|
368
|
+
path_frequency.columns = ["path_id", "path_count"]
|
|
369
|
+
path_data = pd.merge(
|
|
370
|
+
path_frequency,
|
|
371
|
+
stop_times_df.drop_duplicates(subset="path_id"),
|
|
372
|
+
on="path_id",
|
|
373
|
+
)
|
|
374
|
+
path_data_dict = path_data.to_dict(orient="records")
|
|
375
|
+
|
|
376
|
+
return [
|
|
377
|
+
{
|
|
378
|
+
"type": "Feature",
|
|
379
|
+
"geometry": {
|
|
380
|
+
"type": "LineString",
|
|
381
|
+
"coordinates": (
|
|
382
|
+
path["prev_similar_stops_centroid"],
|
|
383
|
+
path["next_similar_stops_centroid"],
|
|
384
|
+
),
|
|
385
|
+
},
|
|
386
|
+
"properties": {
|
|
387
|
+
"frequency": path["path_count"],
|
|
388
|
+
"prev_stop_id": path["prev_stop_id"],
|
|
389
|
+
"prev_stop_name": path["prev_stop_name"],
|
|
390
|
+
"next_stop_id": path["next_stop_id"],
|
|
391
|
+
"next_stop_name": path["next_stop_name"],
|
|
392
|
+
"agency_id": path["agency_id"],
|
|
393
|
+
"agency_name": path["agency_name"],
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
for path in path_data_dict
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
def __get_trips_on_a_date(self, yyyymmdd: str):
|
|
400
|
+
"""
|
|
401
|
+
get trips are on service on a date.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
yyyymmdd (str): [description]
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
[type]: [description]
|
|
408
|
+
"""
|
|
409
|
+
# sunday, monday, tuesday...
|
|
410
|
+
day_of_week = (
|
|
411
|
+
datetime.date(int(yyyymmdd[0:4]), int(yyyymmdd[4:6]), int(yyyymmdd[6:8]))
|
|
412
|
+
.strftime("%A")
|
|
413
|
+
.lower()
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# filter services by day
|
|
417
|
+
calendar_df = self.gtfs["calendar"].copy()
|
|
418
|
+
calendar_df = calendar_df.astype({"start_date": int, "end_date": int})
|
|
419
|
+
calendar_df = calendar_df[calendar_df[day_of_week] == "1"]
|
|
420
|
+
calendar_df = calendar_df.query(
|
|
421
|
+
f"start_date <= {int(yyyymmdd)} and {int(yyyymmdd)} <= end_date",
|
|
422
|
+
engine="python",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
services_on_a_day = calendar_df[["service_id"]]
|
|
426
|
+
|
|
427
|
+
calendar_dates_df = self.gtfs.get("calendar_dates")
|
|
428
|
+
if calendar_dates_df is not None:
|
|
429
|
+
filtered = calendar_dates_df[calendar_dates_df["date"] == yyyymmdd][
|
|
430
|
+
["service_id", "exception_type"]
|
|
431
|
+
]
|
|
432
|
+
to_be_removed_services = filtered[filtered["exception_type"] == "2"]
|
|
433
|
+
to_be_appended_services = filtered[filtered["exception_type"] == "1"][
|
|
434
|
+
["service_id"]
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
services_on_a_day = pd.merge(
|
|
438
|
+
services_on_a_day, to_be_removed_services, on="service_id", how="left"
|
|
439
|
+
)
|
|
440
|
+
services_on_a_day = services_on_a_day[
|
|
441
|
+
services_on_a_day["exception_type"] != "2"
|
|
442
|
+
]
|
|
443
|
+
services_on_a_day = pd.concat([services_on_a_day, to_be_appended_services])
|
|
444
|
+
|
|
445
|
+
services_on_a_day["service_flag"] = 1
|
|
446
|
+
|
|
447
|
+
# filter trips
|
|
448
|
+
trips_df = self.gtfs["trips"].copy()
|
|
449
|
+
trip_service = pd.merge(trips_df, services_on_a_day, on="service_id")
|
|
450
|
+
trip_service = trip_service[trip_service["service_flag"] == 1]
|
|
451
|
+
|
|
452
|
+
return trip_service[["trip_id", "service_flag"]]
|
gtfs_parser/gtfs.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def GTFS(gtfs_dir: list) -> dict:
|
|
8
|
+
tables = {}
|
|
9
|
+
table_files = glob.glob(os.path.join(gtfs_dir, "*.txt"))
|
|
10
|
+
for table_file in table_files:
|
|
11
|
+
datatype = os.path.basename(table_file).split(".")[0]
|
|
12
|
+
with open(table_file, encoding="utf-8_sig") as f:
|
|
13
|
+
df = pd.read_csv(f, dtype=str)
|
|
14
|
+
if len(df) == 0:
|
|
15
|
+
print(f"{datatype}.txt is empty, skipping...")
|
|
16
|
+
continue
|
|
17
|
+
tables[datatype] = df
|
|
18
|
+
|
|
19
|
+
# cast some numeric columns from str to numeric
|
|
20
|
+
tables["stops"] = tables["stops"].astype({"stop_lon": float, "stop_lat": float})
|
|
21
|
+
tables["stop_times"] = tables["stop_times"].astype({"stop_sequence": int})
|
|
22
|
+
if tables.get("shapes") is not None:
|
|
23
|
+
tables["shapes"] = tables["shapes"].astype(
|
|
24
|
+
{"shape_pt_lon": float, "shape_pt_lat": float, "shape_pt_sequence": int}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# parent_station is optional column on GTFS but use in this module
|
|
28
|
+
# when parent_station is not in stops, fill by 'nan' (not NaN)
|
|
29
|
+
if "parent_station" not in tables.get("stops").columns:
|
|
30
|
+
tables["stops"]["parent_station"] = "nan"
|
|
31
|
+
|
|
32
|
+
return tables
|
gtfs_parser/parse.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def read_stops(gtfs:dict, ignore_no_route=False) -> list:
|
|
4
|
+
"""
|
|
5
|
+
read stops by stops table
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
ignore_no_route (bool, optional): stops unconnected to routes are skipped. Defaults to False.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
list: [description]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# get unique list of route_id related to each stop
|
|
15
|
+
stop_times_trip_df = pd.merge(
|
|
16
|
+
gtfs["stop_times"],
|
|
17
|
+
gtfs["trips"],
|
|
18
|
+
on="trip_id",
|
|
19
|
+
)
|
|
20
|
+
route_ids_on_stops = stop_times_trip_df.groupby("stop_id")["route_id"].unique()
|
|
21
|
+
route_ids_on_stops.apply(lambda x: x.sort())
|
|
22
|
+
|
|
23
|
+
# parse stops to GeoJSON-Features
|
|
24
|
+
features = []
|
|
25
|
+
for stop in gtfs["stops"][
|
|
26
|
+
["stop_id", "stop_lat", "stop_lon", "stop_name"]
|
|
27
|
+
].itertuples():
|
|
28
|
+
# get all route_id related to the stop
|
|
29
|
+
route_ids = []
|
|
30
|
+
if stop.stop_id in route_ids_on_stops:
|
|
31
|
+
route_ids = route_ids_on_stops.at[stop.stop_id].tolist()
|
|
32
|
+
|
|
33
|
+
if len(route_ids) == 0 and ignore_no_route:
|
|
34
|
+
# skip to output the stop
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
features.append(
|
|
38
|
+
{
|
|
39
|
+
"type": "Feature",
|
|
40
|
+
"geometry": {
|
|
41
|
+
"type": "Point",
|
|
42
|
+
"coordinates": (stop.stop_lon, stop.stop_lat),
|
|
43
|
+
},
|
|
44
|
+
"properties": {
|
|
45
|
+
"stop_id": stop.stop_id,
|
|
46
|
+
"stop_name": stop.stop_name,
|
|
47
|
+
"route_ids": route_ids,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
return features
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_routes(gtfs:dict, ignore_shapes=False) -> list:
|
|
55
|
+
"""
|
|
56
|
+
read routes by shapes or stop_times
|
|
57
|
+
First, this method try to load shapes and parse it into routes,
|
|
58
|
+
but shapes is optional table in GTFS. Then is shapes does not exist or no_shapes is True,
|
|
59
|
+
this parse routes by stop_time, stops, trips, and routes.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
no_shapes (bool, optional): ignore shapes table. Defaults to False.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
[list]: list of GeoJSON-Feature-dict
|
|
66
|
+
"""
|
|
67
|
+
features = []
|
|
68
|
+
|
|
69
|
+
if gtfs.get("shapes") is None or ignore_shapes:
|
|
70
|
+
# trip-route-merge:A
|
|
71
|
+
trips_routes = pd.merge(
|
|
72
|
+
gtfs["trips"][["trip_id", "route_id"]],
|
|
73
|
+
gtfs["routes"][
|
|
74
|
+
["route_id", "route_long_name", "route_short_name"]
|
|
75
|
+
],
|
|
76
|
+
on="route_id",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# stop_times-stops-merge:B
|
|
80
|
+
stop_times_stop = pd.merge(
|
|
81
|
+
gtfs["stop_times"][["stop_id", "trip_id", "stop_sequence"]],
|
|
82
|
+
gtfs.get("stops")[["stop_id", "stop_lon", "stop_lat"]],
|
|
83
|
+
on="stop_id",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# A-B-merge
|
|
87
|
+
merged = pd.merge(stop_times_stop, trips_routes, on="trip_id")
|
|
88
|
+
merged["route_concat_name"] = merged["route_long_name"].fillna("") + merged[
|
|
89
|
+
"route_short_name"
|
|
90
|
+
].fillna("")
|
|
91
|
+
|
|
92
|
+
# parse routes
|
|
93
|
+
for route_id in merged["route_id"].unique():
|
|
94
|
+
route = merged[merged["route_id"] == route_id]
|
|
95
|
+
trip_id = route["trip_id"].unique()[0]
|
|
96
|
+
route = route[route["trip_id"] == trip_id].sort_values("stop_sequence")
|
|
97
|
+
features.append(
|
|
98
|
+
{
|
|
99
|
+
"type": "Feature",
|
|
100
|
+
"geometry": {
|
|
101
|
+
"type": "LineString",
|
|
102
|
+
"coordinates": route[
|
|
103
|
+
["stop_lon", "stop_lat"]
|
|
104
|
+
].values.tolist(),
|
|
105
|
+
},
|
|
106
|
+
"properties": {
|
|
107
|
+
"route_id": str(route_id),
|
|
108
|
+
"route_name": route.route_concat_name.values.tolist()[0],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
# get_shapeids_on route
|
|
114
|
+
trips_with_shape_df = gtfs["trips"][["route_id", "shape_id"]].dropna(
|
|
115
|
+
subset=["shape_id"]
|
|
116
|
+
)
|
|
117
|
+
shape_ids_on_routes = trips_with_shape_df.groupby("route_id")["shape_id"].unique()
|
|
118
|
+
shape_ids_on_routes.apply(lambda x: x.sort())
|
|
119
|
+
|
|
120
|
+
#get shape coordinate
|
|
121
|
+
shapes_df = gtfs["shapes"].copy()
|
|
122
|
+
shapes_df.sort_values("shape_pt_sequence")
|
|
123
|
+
shapes_df["pt"] = shapes_df[["shape_pt_lon", "shape_pt_lat"]].values.tolist()
|
|
124
|
+
shape_coords = shapes_df.groupby("shape_id")["pt"].apply(tuple)
|
|
125
|
+
|
|
126
|
+
# list-up already loaded shape_ids
|
|
127
|
+
loaded_shape_ids = set()
|
|
128
|
+
for route in gtfs.get("routes").itertuples():
|
|
129
|
+
if shape_ids_on_routes.get(route.route_id) is None:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# get coords by route_id
|
|
133
|
+
coordinates = []
|
|
134
|
+
for shape_id in shape_ids_on_routes[route.route_id]:
|
|
135
|
+
coordinates.append(shape_coords.at[shape_id])
|
|
136
|
+
loaded_shape_ids.add(shape_id) # update loaded shape_ids
|
|
137
|
+
|
|
138
|
+
#get_route_name_from_tupple
|
|
139
|
+
if not pd.isna(route.route_short_name):
|
|
140
|
+
route_name = route.route_short_name
|
|
141
|
+
elif not pd.isna(route.route_long_name):
|
|
142
|
+
route_name = route.route_long_name
|
|
143
|
+
else:
|
|
144
|
+
ValueError(f'{route} have neither "route_long_name" or "route_short_time".')
|
|
145
|
+
|
|
146
|
+
features.append(
|
|
147
|
+
{
|
|
148
|
+
"type": "Feature",
|
|
149
|
+
"geometry": {
|
|
150
|
+
"type": "MultiLineString",
|
|
151
|
+
"coordinates": coordinates,
|
|
152
|
+
},
|
|
153
|
+
"properties": {
|
|
154
|
+
"route_id": str(route.route_id),
|
|
155
|
+
"route_name": route_name,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# load shapes unloaded yet
|
|
161
|
+
for shape_id in list(
|
|
162
|
+
filter(lambda id: id not in loaded_shape_ids, shape_coords.index)
|
|
163
|
+
):
|
|
164
|
+
features.append(
|
|
165
|
+
{
|
|
166
|
+
"type": "Feature",
|
|
167
|
+
"geometry": {
|
|
168
|
+
"type": "MultiLineString",
|
|
169
|
+
"coordinates": [shape_coords.at[shape_id]],
|
|
170
|
+
},
|
|
171
|
+
"properties": {
|
|
172
|
+
"route_id": None,
|
|
173
|
+
"route_name": str(shape_id),
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return features
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2020 MIERUNE Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gtfs-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: parse and aggregate GTFS
|
|
5
|
+
Home-page: https://github.com/MIERUNE
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: MIERUNE Inc.
|
|
8
|
+
Requires-Python: >=3.7.1
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Dist: pandas (>=1.3.3)
|
|
16
|
+
Project-URL: Repository, https://github.com/MIERUNE/{project-name}
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# gtfs_parser
|
|
20
|
+
|
|
21
|
+
## LICENSE
|
|
22
|
+
|
|
23
|
+
MIT License
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
TODO
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
TODO
|
|
32
|
+
|
|
33
|
+
## CLI
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
usage: __main__.py [-h] [--parse_ignoreshapes] [--parse_ignorenoroute]
|
|
37
|
+
[--aggregate_yyyymmdd AGGREGATE_YYYYMMDD]
|
|
38
|
+
[--aggregate_nounifystops]
|
|
39
|
+
[--aggregate_delimiter AGGREGATE_DELIMITER]
|
|
40
|
+
[--aggregate_begintime AGGREGATE_BEGINTIME]
|
|
41
|
+
[--aggregate_endtime AGGREGATE_ENDTIME]
|
|
42
|
+
mode src dst
|
|
43
|
+
|
|
44
|
+
positional arguments:
|
|
45
|
+
mode
|
|
46
|
+
src
|
|
47
|
+
dst
|
|
48
|
+
|
|
49
|
+
optional arguments:
|
|
50
|
+
-h, --help show this help message and exit
|
|
51
|
+
--parse_ignoreshapes
|
|
52
|
+
--parse_ignorenoroute
|
|
53
|
+
--aggregate_yyyymmdd AGGREGATE_YYYYMMDD
|
|
54
|
+
--aggregate_nounifystops
|
|
55
|
+
--aggregate_delimiter AGGREGATE_DELIMITER
|
|
56
|
+
--aggregate_begintime AGGREGATE_BEGINTIME
|
|
57
|
+
--aggregate_endtime AGGREGATE_ENDTIME
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Example
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
python -m gtfs_parser parse gtfs.zip output
|
|
64
|
+
python -m gtfs_parser parse gtfs_dir output --parse_ignoreshapes
|
|
65
|
+
python -m gtfs_parser aggregate gtfs.zip output
|
|
66
|
+
python -m gtfs_parser aggregate gtfs_dir output --aggregate_nounifystops
|
|
67
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
gtfs_parser/__init__.py,sha256=AnaeEmVmXQi-AK-U3ZlPedWe-FKi3WXQ-Mj_nWtQLm8,54
|
|
2
|
+
gtfs_parser/__main__.py,sha256=zfE3xyT62Clu1u13q5ESktatcLhaeFOP7md2dVrmjAY,4198
|
|
3
|
+
gtfs_parser/aggregate.py,sha256=WoXZCPYNso0Y1D2xUHcwfYtmrVdWnY_MmetHWM3vSJA,17228
|
|
4
|
+
gtfs_parser/gtfs.py,sha256=rPPEzOPVP3h0wbRzgJutZ5uaX3gDcMlI9Ug7zI9mBqc,1178
|
|
5
|
+
gtfs_parser/parse.py,sha256=9jb32aeu7rMVfl-65KL5cGzAg26ye_CAoKyaEpzk_D4,6135
|
|
6
|
+
gtfs_parser-0.1.0.dist-info/LICENSE,sha256=zLQWXGgmFXub4HDAPU-w8vvRUFSX7tBJszPoYnvN7e0,1051
|
|
7
|
+
gtfs_parser-0.1.0.dist-info/METADATA,sha256=ZFwFJzxzTN9gljr881-5SLqnQOpLWuYXZi7LIu4Link,1732
|
|
8
|
+
gtfs_parser-0.1.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
9
|
+
gtfs_parser-0.1.0.dist-info/entry_points.txt,sha256=UaB49aNvntx4NNQTZ-3G25l2vvghejTcsmhPBXQHxWE,57
|
|
10
|
+
gtfs_parser-0.1.0.dist-info/RECORD,,
|