7 from copy import deepcopy
8 from datetime import timedelta
9 from typing import List, NamedTuple, Optional
14 from osgeo.ogr import Layer, Geometry, wkbPoint, Feature
15 from osgeo.osr import SpatialReference, CoordinateTransformation, OAMS_TRADITIONAL_GIS_ORDER
17 from wrpylib.json_tools import order_json_keys, format_json
18 from wrpylib.mwapi import WikiSite, page_json
19 from wrpylib.vao import Vao
22 class DistInfo(NamedTuple):
30 class VaoError(RuntimeError):
34 def try_vao_car_distance(vao: Vao, parking_lon: float, parking_lat: float, city: Feature) -> DistInfo:
35 """may throw VaoError with JSON decoded response as argument"""
36 geometry = city.GetGeometryRef()
37 point = geometry.GetPoint(0)
38 city_lon, city_lat, _ = point
40 'originCoordLat': city_lat,
41 'originCoordLong': city_lon,
42 'destCoordLat': parking_lat,
43 'destCoordLong': parking_lon,
44 'groupFilter': 'API_CAR',
45 'totalCar': '1|evnt=0,aevnt=0,tsta=0,htsta=0,getInitEndTimes=0',
47 response = vao.trip(parameter).json()
48 trip = response.get('Trip', [])
50 leg_list = trip[0].get('LegList', {}).get('Leg', [])
53 duration = isodate.parse_duration(leg['duration'])
54 duration_minutes = int(round(duration / timedelta(minutes=1)))
56 co2_kg = trip[0]['Eco']['co2']
57 return DistInfo(city['name'], city['geonames_id'], duration_minutes, dist_m, co2_kg)
58 raise VaoError(response)
61 def vao_car_distance(vao: Vao, parking_lon: float, parking_lat: float, city: Feature, retry_count: int = 2) -> DistInfo:
62 for c in range(retry_count):
64 return try_vao_car_distance(vao, parking_lon, parking_lat, city)
65 except VaoError as vao_error:
66 response = vao_error.args[0]
67 if response.get('errorCode') is not None:
68 print(response['errorCode'], response.get('errorText'), f'(attempt {c+1}/{retry_count})')
69 if response['errorCode'] == 'SVC_NO_RESULT':
73 print('Unexpected result from VAO')
77 def distance_meter(a: Geometry, b: Geometry) -> float:
78 spatial_reference_ll = SpatialReference()
79 spatial_reference_ll.ImportFromEPSG(4326)
80 spatial_reference_ll.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER)
82 spatial_reference_m = SpatialReference()
83 spatial_reference_m.ImportFromProj4(f'+proj=merc +lat_ts={(a.GetY() + b.GetY()) / 2}')
85 ll_to_m = CoordinateTransformation(spatial_reference_ll, spatial_reference_m)
88 a_m.Transform(ll_to_m)
91 b_m.Transform(ll_to_m)
93 return a_m.Distance(b_m)
96 def dist_info_to_dict(dist_info: DistInfo) -> dict:
98 'km': round(dist_info.dist_m / 1000, 1),
99 'route': dist_info.city_name,
100 'minutes': dist_info.duration_minutes,
101 'geonames_id': dist_info.geoname_id,
102 'onward_co2_kg': round(dist_info.co2_kg, 1),
106 def update_sledrun(vao: Vao, db_cities: Layer, site: WikiSite, title: str):
107 sledrun_json_page = site.query_page(f'{title}/Rodelbahn.json')
108 sledrun_json = page_json(sledrun_json_page)
110 sledrun_json_orig = sledrun_json.copy()
112 car_parking = sledrun_json.get('car_parking')
114 print(' (no parking)')
118 parking = car_parking[0].get('position', {}).get('position')
121 parking_lon = parking['longitude']
122 parking_lat = parking['latitude']
124 car_distance_list = deepcopy(sledrun_json.get('car_distances', []))
125 if len([car_distance for car_distance in car_distance_list if car_distance.get('geonames_id') is not None]) > 0:
128 db_cities.SetSpatialFilter(None)
129 db_cities.SetAttributeFilter(None)
130 for car_distance in car_distance_list:
131 if car_distance.get('geonames_id') is None:
132 name = car_distance['route']
133 match = re.match(r'([-\w. ]+)\(?.*$', name)
134 if match is not None:
135 name = match.group(1).strip()
136 candidates = [city for city in db_cities if city['name'] == name]
137 if len(candidates) == 1:
139 dist_info = vao_car_distance(vao, parking_lon, parking_lat, city)
140 if dist_info is not None:
141 dist_info_dict = dist_info_to_dict(dist_info)
142 car_distance.update(dist_info_dict)
144 spatial_reference_ll = SpatialReference()
145 spatial_reference_ll.ImportFromEPSG(4326)
146 spatial_reference_ll.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER)
148 spatial_reference_m = SpatialReference()
149 spatial_reference_m.ImportFromProj4(f'+proj=merc +lat_ts={parking_lat}')
151 ll_to_m = CoordinateTransformation(spatial_reference_ll, spatial_reference_m)
152 m_to_ll = CoordinateTransformation(spatial_reference_m, spatial_reference_ll)
154 parking_ll = Geometry(wkbPoint)
155 parking_ll.AddPoint(parking_lon, parking_lat)
157 parking_m = parking_ll.Clone()
158 parking_m.Transform(ll_to_m)
160 bound_m = parking_m.Buffer(max_dist_m)
161 bound_ll = bound_m.Clone()
162 bound_ll.Transform(m_to_ll)
164 db_cities.SetSpatialFilter(bound_ll)
165 db_cities.SetAttributeFilter('level<=2')
166 close_cities = [city for city in db_cities]
167 close_cities = sorted(close_cities, key=lambda city: distance_meter(city.GetGeometryRef(), parking_ll))
169 max_number_distances = 3
170 car_distances_to_append: List[dict] = []
171 for city in close_cities:
172 if len(car_distances_to_append) >= max_number_distances:
173 if car_distances_to_append[2]['km'] * 1000 < distance_meter(city.GetGeometryRef(), parking_ll):
176 car_distance = next((cd for cd in car_distance_list if cd.get('geonames_id') == city['geonames_id']), None)
177 if car_distance is None:
178 car_distance = vao_car_distance(vao, parking_lon, parking_lat, city)
179 if car_distance is not None:
180 car_distance = dist_info_to_dict(car_distance)
181 if car_distance is not None:
182 car_distances_to_append.append(car_distance)
183 car_distances_to_append = sorted(car_distances_to_append, key=lambda di: di['km'])
185 car_distances_to_append = sorted(car_distances_to_append, key=lambda di: di['km'])[:max_number_distances]
187 for car_distance in car_distances_to_append:
188 if len([cd for cd in car_distance_list if cd.get('geonames_id') == car_distance['geonames_id']]) == 0:
189 car_distance_list.append(car_distance)
191 car_distance_list = sorted(car_distance_list, key=lambda di: di['km'])
192 sledrun_json['car_distances'] = car_distance_list
194 if sledrun_json == sledrun_json_orig:
197 jsonschema.validate(instance=sledrun_json, schema=site.sledrun_schema())
198 sledrun_json_ordered = order_json_keys(sledrun_json, site.sledrun_schema())
199 assert sledrun_json_ordered == sledrun_json
200 sledrun_json_str = format_json(sledrun_json_ordered)
204 pageid=sledrun_json_page['pageid'],
205 text=sledrun_json_str,
206 summary=f'Entfernungen zu {title} aktualisiert (dank VAO).',
209 baserevid=sledrun_json_page['revisions'][0]['revid'],
215 def update_car_distances(ini_files: List[str]):
218 config = configparser.ConfigParser()
219 config.read(ini_files)
220 host = config.get('mysql', 'host')
221 dbname = config.get('mysql', 'dbname')
222 user = config.get('mysql', 'user_name')
223 passwd = config.get('mysql', 'user_pass')
225 cities_wr_source = ogr.Open(f'MySQL:{dbname},"host={host}","user={user}","password={passwd}","tables=wrcity"')
226 db_cities = cities_wr_source.GetLayerByIndex(0)
228 site = WikiSite(ini_files)
229 vao = Vao(config.get('vao', 'access_id'))
230 for result in site.query(list='categorymembers', cmtitle='Kategorie:Rodelbahn', cmlimit='max'):
231 for page in result['categorymembers']:
233 if page['title'] in ['Anzère', 'Hochhäderich (Falkenhütte)', 'Hochlitten-Moosalpe', 'Saas-Fee']:
235 update_sledrun(vao, db_cities, site, page['title'])
239 parser = argparse.ArgumentParser(description='Update car distance information in sledrun JSON files.')
240 parser.add_argument('inifile', nargs='+', help='inifile.ini, see: https://www.winterrodeln.org/trac/wiki/ConfigIni')
241 args = parser.parse_args()
242 update_car_distances(args.inifile)
245 if __name__ == '__main__':