6 from dataclasses import dataclass
7 from datetime import date, timedelta, datetime
8 from itertools import groupby
9 from typing import List, Iterable, Optional, Set, Tuple, Dict
13 from termcolor import cprint # python3-termcolor
15 from wrpylib.cli_tools import unified_diff, input_yes_no_quit, Choice
16 from wrpylib.json_tools import order_json_keys, format_json
17 from wrpylib.lib_update_public_transport import default_query_date, vao_ext_id_to_ifopt_stop_id
18 from wrpylib.mwapi import WikiSite, page_json
19 from wrpylib.vao import Vao
20 from wrpylib.wrvalidators import LonLat
23 class VaoError(RuntimeError):
30 dist_m: int # Luftlinie
38 vao_line_id: str # e.g. 'vvt-8-325-T-j23-1'
39 line: Optional[str] # e.g. '325T', some trains don't have line labels.
40 category: str # e.g. 'Anrufsammeltaxi'
41 operator: str # e.g. 'Quaxis Taxi & Busreisen'
44 def get_vvt_stop_id(ext_id: str) -> Optional[int]:
45 vvt_stop_id_match = re.match(r'^47(\d{5})00$', ext_id)
47 return int(vvt_stop_id_match.group(1))
51 def try_vao_nearby_stops(vao: Vao, lon_lat: LonLat) -> Tuple[List[StopInfo], Dict[str, LineInfo]]:
52 """may throw VaoError with JSON decoded response as argument"""
54 'originCoordLat': lon_lat.lat,
55 'originCoordLong': lon_lat.lon,
57 response = vao.get('location.nearbystops', parameter).json()
58 stop_info_list: List[StopInfo] = []
59 line_info_dict: Dict[str, LineInfo] = {}
60 for stop in response.get('stopLocationOrCoordLocation', []):
61 sl = stop['StopLocation']
63 for ps in sl['productAtStop']:
64 vao_line_id = ps['lineId']
65 line_ids.add(vao_line_id)
66 line_info_dict[vao_line_id] = LineInfo(
67 vao_line_id=vao_line_id,
69 category=ps['catOutL'],
70 operator=ps['operator'],
72 ext_id = sl['mainMastExtId']
73 stop_info_list.append(StopInfo(sl['name'], sl['dist'], LonLat(sl['mainMastLon'], sl['mainMastLat']), line_ids, ext_id))
74 return stop_info_list, line_info_dict
77 def merge_stops(stops: List[StopInfo]) -> StopInfo:
78 mean_dist = int(round(sum(stop.dist_m for stop in stops) / len(stops)))
79 all_lines = set().union(*[stop.line_ids for stop in stops])
81 return StopInfo(stop.name, mean_dist, stop.lon_lat, all_lines, stop.ext_id)
84 def unique_nearby_stops(stops: Iterable[StopInfo]) -> List[StopInfo]:
85 stops = sorted(stops, key=lambda stop: stop.ext_id)
86 stops = [merge_stops(list(i)) for k, i in groupby(stops, key=lambda stop: stop.ext_id)]
87 return sorted(stops, key=lambda stop: stop.dist_m)
96 def try_vao_walk_distance(vao: Vao, origin: LonLat, dest: LonLat) -> WalkDistInfo:
97 """may throw VaoError with JSON decoded response as argument"""
99 'originCoordLat': origin.lat,
100 'originCoordLong': origin.lon,
101 'destCoordLat': dest.lat,
102 'destCoordLong': dest.lon,
103 'groupFilter': 'API_WALK',
105 response = vao.trip(parameter).json()
106 trip = response.get('Trip', [])
108 leg_list = trip[0].get('LegList', {}).get('Leg', [])
111 duration = isodate.parse_duration(leg['duration'])
112 duration_minutes = int(round(duration / timedelta(minutes=1)))
114 return WalkDistInfo(dist_m, duration_minutes)
115 raise VaoError(response)
120 vao_line_id: str # e.g. 'vvt-8-325-T-j23-1'
122 direction: str # e.g. 'Innsbruck Hauptbahnhof'
123 flag: str # e.g. 'H' or 'R'
127 def try_vao_departure_board(vao: Vao, ext_id: str, journey_date: datetime, journey_minutes: int,
128 line_info_dict: Dict[str, LineInfo]) -> List[Departure]:
131 'date': journey_date.date().isoformat(),
132 'time': journey_date.time().isoformat('minutes'),
133 'duration': journey_minutes,
135 response = vao.get('departureBoard', parameter).json()
137 for departure in response.get('Departure', []):
138 ps = departure['ProductAtStop']
139 time = datetime.fromisoformat(f'{departure["date"]}T{departure["time"]}')
140 vao_line_id = ps['lineId']
141 departure_list.append(Departure(vao_line_id, time, departure['direction'],
142 departure['directionFlag'], departure['JourneyDetailRef']['ref']))
143 line_info_dict[vao_line_id] = LineInfo(
144 vao_line_id=vao_line_id,
146 category=ps['catOutL'],
147 operator=ps['operator'],
149 return departure_list
154 vao_line_id: str # e.g. 'vvt-8-325-T-j23-1'
156 origin: str # e.g. 'Innsbruck Hauptbahnhof'
160 def try_vao_arrival_board(vao: Vao, ext_id: str, journey_date: datetime, journey_minutes: int,
161 line_info_dict: Dict[str, LineInfo]) -> List[Arrival]:
164 'date': journey_date.date().isoformat(),
165 'time': journey_date.time().isoformat('minutes'),
166 'duration': journey_minutes,
168 response = vao.get('arrivalBoard', parameter).json()
170 for arrival in response.get('Arrival', []):
171 ps = arrival['ProductAtStop']
172 time = datetime.fromisoformat(f'{arrival["date"]}T{arrival["time"]}')
173 vao_line_id = ps['lineId']
174 arrival_list.append(Arrival(vao_line_id, time, arrival['origin'], arrival['JourneyDetailRef']['ref']))
175 line_info_dict[vao_line_id] = LineInfo(
176 vao_line_id=vao_line_id,
178 category=ps['catOutL'],
179 operator=ps['operator'],
184 def decode_service_days(value: str, begin: date) -> Iterable[date]:
185 days_of_service = ''.join(f'{int(i, 16):04b}' for i in value)
186 days_of_service = [bool(int(i)) for i in days_of_service]
187 for i, day_has_service in enumerate(days_of_service):
189 yield begin + timedelta(days=i)
194 planning_period_begin: date
195 planning_period_end: date
196 day_of_operation: date
200 service_days: List[date]
205 def try_vao_journey_detail(vao: Vao, journey_detail_ref: str) -> JourneyDetail:
207 'id': journey_detail_ref,
210 response = vao.get('journeyDetail', parameter).json()
211 sd = response['ServiceDays'][0]
212 stops = response['Stops']['Stop']
213 planning_period_begin = date.fromisoformat(sd['planningPeriodBegin'])
214 s_days_b = sd['sDaysB']
215 service_days = list(decode_service_days(s_days_b, planning_period_begin))
216 return JourneyDetail(planning_period_begin, date.fromisoformat(sd['planningPeriodEnd']),
217 response['dayOfOperation'], sd['sDaysR'], sd['sDaysI'], s_days_b,
218 service_days, stops[0]['name'], stops[-1]['name'])
227 def is_redundant_stop(stop: StopWithDist, stops: List[StopWithDist]) -> bool:
230 if stop.stop.line_ids.issubset(s.stop.line_ids):
235 def remove_redundant_stops(stops: List[StopWithDist]) -> List[StopWithDist]:
240 if not is_redundant_stop(stop, stops):
241 result.insert(0, stop)
245 def update_sledrun(vao: Vao, site: WikiSite, title: str, query_date: date, missing_only: bool, no_schedules: bool):
246 sledrun_json_page = site.query_page(f'{title}/Rodelbahn.json')
247 sledrun_json = page_json(sledrun_json_page)
249 sledrun_json_orig = sledrun_json.copy()
252 if pt_stops := sledrun_json.get('public_transport_stops'):
253 if len(pt_stops) > 0 and pt_stops[0].get('ifopt_stop_id'):
256 pos = sledrun_json.get('bottom', {}).get('position')
259 lon_lat = LonLat(pos['longitude'], pos['latitude'])
260 nearby_stops, line_info_dict = try_vao_nearby_stops(vao, lon_lat)
261 nearby_stops = unique_nearby_stops(nearby_stops)
262 stops_with_dists = [StopWithDist(stop, try_vao_walk_distance(vao, stop.lon_lat, lon_lat)) for stop in nearby_stops]
263 stops_with_dists = sorted(stops_with_dists, key=lambda s: s.dist.dist_m)
264 stops_with_dists = remove_redundant_stops(stops_with_dists)
266 journey_date = datetime(query_date.year, query_date.month, query_date.day)
267 journey_minutes = 1439
269 public_transport_stops = []
271 for stop_with_dist in stops_with_dists:
273 departures = try_vao_departure_board(vao, stop_with_dist.stop.ext_id, journey_date, journey_minutes,
275 arrivals = try_vao_arrival_board(vao, stop_with_dist.stop.ext_id, journey_date, journey_minutes, line_info_dict)
276 # journey_detail_ref = arrivals[41].detail_ref
277 # journey_detail = try_vao_journey_detail(vao, journey_detail_ref)
279 vao_line_ids = set(a.vao_line_id for a in arrivals).union(d.vao_line_id for d in departures) \
280 .union(stop_with_dist.stop.line_ids)
283 for vao_line_id in vao_line_ids:
284 line_info = line_info_dict[vao_line_id]
286 for direction in set(d.direction for d in departures if d.vao_line_id == vao_line_id):
288 "direction": direction,
289 "datetime": [d.time.isoformat(timespec='minutes') for d in departures
290 if d.vao_line_id == vao_line_id and d.direction == direction]
294 for origin in set(a.origin for a in arrivals if a.vao_line_id == vao_line_id):
297 "datetime": [a.time.isoformat(timespec='minutes') for a in arrivals
298 if a.vao_line_id == vao_line_id and a.origin == origin]
302 'service_date': journey_date.date().isoformat(),
303 "day_type": "work_day",
304 "departure": departure,
309 "vao_line_id": vao_line_id,
310 "line": line_info.line,
311 "category": line_info.category,
312 "operator": line_info.operator,
313 "schedules": schedules,
317 public_transport_stop = {
318 "name": stop_with_dist.stop.name,
319 "vao_ext_id": stop_with_dist.stop.ext_id,
322 "longitude": stop_with_dist.stop.lon_lat.lon,
323 "latitude": stop_with_dist.stop.lon_lat.lat
326 "walk_distance": stop_with_dist.dist.dist_m,
327 "walk_time": stop_with_dist.dist.time_min,
330 public_transport_stop["lines"] = lines
331 if ifopt_stop_id := vao_ext_id_to_ifopt_stop_id(stop_with_dist.stop.ext_id):
332 public_transport_stop['ifopt_stop_id'] = ifopt_stop_id
333 vvt_stop_id = get_vvt_stop_id(stop_with_dist.stop.ext_id)
334 if vvt_stop_id is not None:
335 public_transport_stop["vvt_stop_id"] = vvt_stop_id
337 public_transport_stops.append(public_transport_stop)
339 if len(public_transport_stops) > 0 or 'public_transport_stops' not in sledrun_json:
340 sledrun_json['public_transport_stops'] = public_transport_stops
342 if sledrun_json == sledrun_json_orig:
345 jsonschema.validate(instance=sledrun_json, schema=site.sledrun_schema())
346 sledrun_json_ordered = order_json_keys(sledrun_json, site.sledrun_schema())
347 assert sledrun_json_ordered == sledrun_json
348 sledrun_json_orig_str = format_json(sledrun_json_orig)
349 sledrun_json_str = format_json(sledrun_json_ordered)
351 cprint(title, 'green')
352 unified_diff(sledrun_json_orig_str, sledrun_json_str)
353 choice = input_yes_no_quit('Do you accept the changes [yes, no, quit]? ', None)
354 if choice == Choice.no:
357 if choice == Choice.quit:
362 pageid=sledrun_json_page['pageid'],
363 text=sledrun_json_str,
364 summary=f'Informationen zu Öffentlichen Verkehrsmitteln zu {title} aktualisiert (dank VAO).',
367 baserevid=sledrun_json_page['revisions'][0]['revid'],
373 def update_public_transport(ini_files: List[str], query_date: date, sledrun: Optional[str], missing_only: bool,
375 config = configparser.ConfigParser()
376 config.read(ini_files)
378 site = WikiSite(ini_files)
379 vao = Vao(config.get('vao', 'access_id'))
381 if sledrun is not None:
382 update_sledrun(vao, site, sledrun, query_date, missing_only, no_schedules)
385 for result in site.query(list='categorymembers', cmtitle='Kategorie:Rodelbahn', cmlimit='max'):
386 for page in result['categorymembers']:
387 sledrun = page['title']
389 update_sledrun(vao, site, sledrun, query_date, missing_only, no_schedules)
393 query_date = default_query_date(date.today())
394 parser = argparse.ArgumentParser(description='Update public transport information in sledrun JSON files.')
395 parser.add_argument('--sledrun', help='If given, work on a single sled run page, otherwise at the whole category.')
396 parser.add_argument('--missing', action='store_true',
397 help='If given, only work on stops that have no ifopt_stop_id.')
398 parser.add_argument('--no-schedules', action='store_true', help='If given, don\'t add schedules.')
399 parser.add_argument('--date', type=date.fromisoformat, default=query_date,
400 help='Working week date to query the database.')
401 parser.add_argument('inifile', nargs='+', help='inifile.ini, see: https://www.winterrodeln.org/trac/wiki/ConfigIni')
402 args = parser.parse_args()
403 update_public_transport(args.inifile, args.date, args.sledrun, args.missing, args.no_schedules)
406 if __name__ == '__main__':