Skip to content

Commit 60fc9b4

Browse files
committed
Generate configs for crowd-sourced realtime components
1 parent af537f9 commit 60fc9b4

File tree

9 files changed

+214
-83
lines changed

9 files changed

+214
-83
lines changed

ansible/hosts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,9 @@ woodpecker:
113113
30363331323065643861613364303531653565633833346263313334613032613539386430613364
114114
3137363965316563650a356631343130653036613838646331363336643265333662346534343163
115115
33646436316239336363363339386665353532363336636535333139363937373064
116+
117+
crowd-sourced-realtime:
118+
hosts:
119+
crunchy:
120+
ansible_user: root
121+
ansible_host: crunchy.spline.de
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-FileCopyrightText: 2025 Jonah Brüchert <[email protected]>
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
[Unit]
6+
Description=Delay Tracker
7+
After=network.target
8+
9+
[Service]
10+
User=crowd-sourced-realtime
11+
Group=nogroup
12+
Restart=always
13+
ExecStart=/usr/local/bin/delay-tracker
14+
WorkingDirectory=/var/cache/transitous/out/delay-tracker/
15+
16+
[Install]
17+
WantedBy=multi-user.target
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-FileCopyrightText: 2025 Jonah Brüchert <[email protected]>
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
[Unit]
6+
Description=GPS Collector
7+
After=network.target
8+
9+
[Service]
10+
User=crowd-sourced-realtime
11+
Group=nogroup
12+
Restart=always
13+
ExecStart=/usr/local/bin/transitous-gps-collector
14+
WorkingDirectory=/var/cache/transitous/out/gps-collector/
15+
16+
[Install]
17+
WantedBy=multi-user.target
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SPDX-FileCopyrightText: 2025 Jonah Brüchert <[email protected]>
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
- name: Create crowd-sourced-realtime user
6+
user:
7+
name: crowd-sourced-realtime
8+
9+
- name: Install delay-tracker executable
10+
get_url:
11+
url: https://cloud.ghsq.de/s/BfAQb86e5qZWrf8/download/delay-tracker-x86_64
12+
dest: /usr/local/bin/delay-tracker
13+
mode: '0755'
14+
15+
- name: Install delay-tracker service
16+
copy:
17+
src: delay-tracker.service
18+
dest: /etc/systemd/system/delay-tracker.service
19+
register: install_delay_tracker_service
20+
21+
- name: Install transitous-gps-collector
22+
get_url:
23+
url: https://cloud.ghsq.de/s/8FYsw7eXxfStoD5/download/transitous-gps-collector-x86_64
24+
dest: /usr/local/bin/transitous-gps-collector
25+
mode: '0755'
26+
27+
- name: Install transitous-gps-collector service
28+
copy:
29+
src: transitous-gps-collector.service
30+
dest: /etc/systemd/system/transitous-gps-collector.service
31+
register: install_gps_collector_service
32+
33+
- name: Enable delay-tracker
34+
systemd_service:
35+
name: delay-tracker.service
36+
daemon_reload: true
37+
when: install_delay_tracker_service.changed
38+
39+
- name: Enable transitous-gps-collector
40+
systemd_service:
41+
name: transitous-gps-collector.service
42+
daemon_reload: true
43+
when: install_gps_collector_service.changed
44+
45+
- name: Enable delay-tracker
46+
systemd_service:
47+
name: delay-tracker.service
48+
enabled: true
49+
state: "started"
50+
51+
- name: Enable transitous-gps-collector
52+
systemd_service:
53+
name: transitous-gps-collector.service
54+
enabled: true
55+
state: "started"

feeds/eu.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"name": "flixbus",
1515
"type": "transitland-atlas",
1616
"transitland-atlas-id": "f-u-flixbus",
17-
"fix": true
17+
"fix": true,
18+
"enable-crowd-sourced-realtime": true
1819
},
1920
{
2021
"name": "blablacar-bus",

feeds/lt.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
{
101101
"name": "LTG-Link",
102102
"type": "http",
103-
"url": "https://jbb.ghsq.de/gtfs/lt-ltglink.gtfs.zip"
103+
"url": "https://jbb.ghsq.de/gtfs/lt-ltglink.gtfs.zip",
104+
"enable-crowd-sourced-realtime": true
104105
}
105106
]
106107
}

feeds/me.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"license": {
1313
"spdx-identifier": "ODbL-1.0"
1414
},
15-
"mdb-id": "mdb-2377"
15+
"mdb-id": "mdb-2377",
16+
"enable-crowd-sourced-realtime": true
1617
}
1718
]
1819
}

src/generate-motis-config.py

Lines changed: 110 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import argparse
77
import json
8+
import toml
89
import metadata
910
import os
1011
import shutil
@@ -37,10 +38,19 @@ def find_motis_asset(asset_name: str):
3738
atlas = transitland.Atlas.load(Path("transitland-atlas/"))
3839
mdb = mobilitydatabase.Database.load()
3940

40-
gtfs_feeds: list[dict] = []
41-
gtfsrt_feeds: list[dict] = []
41+
feeds = []
42+
regions: list[tuple[str, metadata.Region]] = []
43+
if len(arguments.regions) == 0:
44+
feeds = feed_dir.glob("*.json")
45+
else:
46+
for region in arguments.regions:
47+
feeds += feed_dir.glob(f"{region}.json")
4248

43-
with open("motis/config.yml") as f:
49+
for feed in sorted(feeds):
50+
region_name = feed.name[: feed.name.rfind(".")]
51+
regions.append((region_name, metadata.Region(json.load(open(feed, "r")))))
52+
53+
with open("configs/motis/config.yml") as f:
4454
yaml = YAML(typ="rt")
4555

4656
config = yaml.load(f)
@@ -66,90 +76,110 @@ def find_motis_asset(asset_name: str):
6676
config["timetable"]["datasets"] = {}
6777
config["gbfs"]["feeds"] = {}
6878

69-
# TODO backward compatibility, remove this in a few months
70-
while "full" in arguments.regions:
71-
print("Ignoring legacy option 'full', this is the default now.")
72-
arguments.regions.remove("full")
73-
74-
feeds = []
75-
if len(arguments.regions) == 0:
76-
feeds = feed_dir.glob("*.json")
77-
else:
78-
for region in arguments.regions:
79-
feeds += feed_dir.glob(f"{region}.json")
80-
81-
for feed in sorted(feeds):
82-
with open(feed, "r") as f:
83-
parsed = json.load(f)
84-
region = metadata.Region(parsed)
85-
86-
metadata_filename = feed.name
87-
region_name = metadata_filename[: metadata_filename.rfind(".")]
88-
89-
for source in region.sources:
90-
schedule_name = f"{region_name}-{source.name}"
91-
92-
if source.skip:
93-
continue
94-
95-
match source:
96-
case metadata.TransitlandSource():
97-
resolved_source = atlas.source_by_id(source)
98-
if not resolved_source:
99-
eprint("Error: Could not resolve", source.transitland_atlas_id)
100-
sys.exit(1)
101-
source = resolved_source
102-
case metadata.MobilityDatabaseSource():
103-
resolved_source = mdb.source_by_id(source)
104-
if not resolved_source:
105-
eprint("Error: Could not resolve", source.mdb_id)
106-
sys.exit(1)
107-
source = resolved_source
108-
109-
match source.spec:
110-
case source.spec if source.spec in ["gtfs", "gtfs-flex"]:
111-
schedule_file = \
112-
f"{region_name}_{source.name}.gtfs.zip"
113-
name = f"{region_name}-{source.name}"
114-
config["timetable"]["datasets"][name] = \
115-
{
116-
"path": schedule_file,
117-
"extend_calendar": source.extend_calendar
118-
}
119-
if source.default_timezone is not None:
120-
config["timetable"]["datasets"][name]["default_timezone"] = source.default_timezone
121-
122-
case "gtfs-rt" if isinstance(source, metadata.UrlSource):
123-
name = f"{region_name}-{source.name}"
124-
if name not in config["timetable"]["datasets"]:
125-
eprint(
126-
"Error: The name of a realtime (gtfs-rt) "
127-
+ "feed needs to match the name of its "
128-
+ "static base feed defined before the "
129-
+ "realtime feed. Found nothing "
130-
+ "belonging to",
131-
source.name,
132-
)
133-
sys.exit(1)
79+
for (region_name, region) in regions:
80+
for source in region.sources:
81+
schedule_name = f"{region_name}-{source.name}"
82+
83+
if source.skip:
84+
continue
85+
86+
match source:
87+
case metadata.TransitlandSource():
88+
resolved_source = atlas.source_by_id(source)
89+
if not resolved_source:
90+
eprint("Error: Could not resolve", source.transitland_atlas_id)
91+
sys.exit(1)
92+
source = resolved_source
93+
case metadata.MobilityDatabaseSource():
94+
resolved_source = mdb.source_by_id(source)
95+
if not resolved_source:
96+
eprint("Error: Could not resolve", source.mdb_id)
97+
sys.exit(1)
98+
source = resolved_source
99+
100+
match source.spec:
101+
case source.spec if source.spec in ["gtfs", "gtfs-flex"]:
102+
schedule_file = \
103+
f"{region_name}_{source.name}.gtfs.zip"
104+
name = f"{region_name}-{source.name}"
105+
config["timetable"]["datasets"][name] = \
106+
{
107+
"path": schedule_file,
108+
"extend_calendar": source.extend_calendar
109+
}
110+
if source.default_timezone is not None:
111+
config["timetable"]["datasets"][name]["default_timezone"] = source.default_timezone
134112

113+
if source.enable_crowd_sourced_realtime:
135114
if "rt" not in config["timetable"]["datasets"][name]:
136115
config["timetable"]["datasets"][name]["rt"] = []
137116

138-
rt_feed: dict[str, Any] = {
139-
"url": source.url
140-
}
117+
config["timetable"]["datasets"][name]["rt"].append({"url": f"http://localhost:5002/{name}/trip-updates.pb" })
118+
119+
case "gtfs-rt" if isinstance(source, metadata.UrlSource):
120+
name = f"{region_name}-{source.name}"
121+
if name not in config["timetable"]["datasets"]:
122+
eprint(
123+
"Error: The name of a realtime (gtfs-rt) "
124+
+ "feed needs to match the name of its "
125+
+ "static base feed defined before the "
126+
+ "realtime feed. Found nothing "
127+
+ "belonging to",
128+
source.name,
129+
)
130+
sys.exit(1)
131+
132+
if "rt" not in config["timetable"]["datasets"][name]:
133+
config["timetable"]["datasets"][name]["rt"] = []
134+
135+
rt_feed: dict[str, Any] = {
136+
"url": source.url
137+
}
141138

142-
if source.headers:
143-
rt_feed["headers"] = source.headers
139+
if source.headers:
140+
rt_feed["headers"] = source.headers
144141

145-
config["timetable"]["datasets"][name]["rt"] \
146-
.append(rt_feed)
142+
config["timetable"]["datasets"][name]["rt"] \
143+
.append(rt_feed)
147144

148-
case "gbfs" if isinstance(source, metadata.UrlSource):
149-
name = f"{region_name}-{source.name}"
150-
config["gbfs"]["feeds"][name] = {"url": source.url}
151-
if source.headers:
152-
config["gbfs"]["feeds"][name]["headers"] = source.headers
145+
case "gbfs" if isinstance(source, metadata.UrlSource):
146+
name = f"{region_name}-{source.name}"
147+
config["gbfs"]["feeds"][name] = {"url": source.url}
148+
if source.headers:
149+
config["gbfs"]["feeds"][name]["headers"] = source.headers
153150

154151
with open("out/config.yml", "w") as fo:
155152
yaml.dump(config, fo)
153+
154+
with open("configs/gps-collector/config.toml", "r") as f:
155+
config = toml.load(f)
156+
config["feeds"] = {}
157+
158+
for (region_name, region) in regions:
159+
for source in region.sources:
160+
if source.enable_crowd_sourced_realtime:
161+
config["feeds"][f"{region_name}-{source.name}"] = {}
162+
163+
if not os.path.exists("out/gps-collector/"):
164+
os.makedirs("out/gps-collector/")
165+
166+
with open("out/gps-collector/config.toml", "w") as fo:
167+
toml.dump(config, fo)
168+
169+
with open("configs/delay-tracker/config.toml", "r") as f:
170+
config = toml.load(f)
171+
config["feeds"] = {}
172+
173+
for (region_name, region) in regions:
174+
for source in region.sources:
175+
if source.enable_crowd_sourced_realtime: # This can be extended for vehicle-positions feeds later on
176+
config["feeds"][f"{region_name}-{source.name}"] = {
177+
"gtfs_url": f"../{region_name}_{source.name}.gtfs.zip",
178+
"gtfsrt_url": f"http://localhost:5001/gtfsrt/{region_name}-{source.name}/vehicle-positions.pb"
179+
}
180+
181+
if not os.path.exists("out/delay-tracker/"):
182+
os.makedirs("out/delay-tracker/")
183+
184+
with open("out/delay-tracker/config.toml", "w") as fo:
185+
toml.dump(config, fo)

src/metadata.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Source:
5656
extend_calendar = False
5757
default_timezone: Optional[str] = None
5858
keep_additional_fields = True
59+
enable_crowd_sourced_realtime = False
5960

6061
def __init__(self, parsed: Optional[dict] = None):
6162
self.license = License()
@@ -96,6 +97,8 @@ def __init__(self, parsed: Optional[dict] = None):
9697
self.default_timezone = parsed["default-timezone"]
9798
if "keep-additional-fields" in parsed:
9899
self.keep_additional_fields = bool(parsed["keep-additional-fields"])
100+
if "enable-crowd-sourced-realtime" in parsed:
101+
self.enable_crowd_sourced_realtime = bool(parsed["enable-crowd-sourced-realtime"])
99102

100103

101104
class HttpOptions:

0 commit comments

Comments
 (0)