]> ToastFreeware Gitweb - philipp/winterrodeln/wrpylib.git/blob - scripts/update_car_distances.py
VAO is missing important streets in Switzerland.
[philipp/winterrodeln/wrpylib.git] / scripts / update_car_distances.py
1 #!/usr/bin/python
2 import argparse
3 import configparser
4 import re
5 import sys
6 import time
7 from copy import deepcopy
8 from datetime import timedelta
9 from typing import List, NamedTuple, Optional
10
11 import isodate
12 import jsonschema
13 from osgeo import ogr
14 from osgeo.ogr import Layer, Geometry, wkbPoint, Feature
15 from osgeo.osr import SpatialReference, CoordinateTransformation, OAMS_TRADITIONAL_GIS_ORDER
16
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
20
21
22 class DistInfo(NamedTuple):
23     city_name: str
24     geoname_id: int
25     duration_minutes: int
26     dist_m: int
27     co2_kg: float
28
29
30 class VaoError(RuntimeError):
31     pass
32
33
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
39     parameter = {
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',
46     }
47     response = vao.trip(parameter).json()
48     trip = response.get('Trip', [])
49     if trip:
50         leg_list = trip[0].get('LegList', {}).get('Leg', [])
51         if leg_list:
52             leg = leg_list[0]
53             duration = isodate.parse_duration(leg['duration'])
54             duration_minutes = int(round(duration / timedelta(minutes=1)))
55             dist_m = leg['dist']
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)
59
60
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):
63         try:
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':
70                     time.sleep(2.)
71                     continue
72             else:
73                 print('Unexpected result from VAO')
74             sys.exit(1)
75
76
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)
81
82     spatial_reference_m = SpatialReference()
83     spatial_reference_m.ImportFromProj4(f'+proj=merc +lat_ts={(a.GetY() + b.GetY()) / 2}')
84
85     ll_to_m = CoordinateTransformation(spatial_reference_ll, spatial_reference_m)
86
87     a_m = a.Clone()
88     a_m.Transform(ll_to_m)
89
90     b_m = b.Clone()
91     b_m.Transform(ll_to_m)
92
93     return a_m.Distance(b_m)
94
95
96 def dist_info_to_dict(dist_info: DistInfo) -> dict:
97     return {
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),
103     }
104
105
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)
109
110     sledrun_json_orig = sledrun_json.copy()
111
112     car_parking = sledrun_json.get('car_parking')
113     if not car_parking:
114         print('  (no parking)')
115         print('')
116         return
117
118     parking = car_parking[0].get('position', {}).get('position')
119     if not parking:
120         return
121     parking_lon = parking['longitude']
122     parking_lat = parking['latitude']
123
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:
126         return
127
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:
138                     city = candidates[0]
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)
143
144     spatial_reference_ll = SpatialReference()
145     spatial_reference_ll.ImportFromEPSG(4326)
146     spatial_reference_ll.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER)
147
148     spatial_reference_m = SpatialReference()
149     spatial_reference_m.ImportFromProj4(f'+proj=merc +lat_ts={parking_lat}')
150
151     ll_to_m = CoordinateTransformation(spatial_reference_ll, spatial_reference_m)
152     m_to_ll = CoordinateTransformation(spatial_reference_m, spatial_reference_ll)
153
154     parking_ll = Geometry(wkbPoint)
155     parking_ll.AddPoint(parking_lon, parking_lat)
156
157     parking_m = parking_ll.Clone()
158     parking_m.Transform(ll_to_m)
159     max_dist_m = 60000
160     bound_m = parking_m.Buffer(max_dist_m)
161     bound_ll = bound_m.Clone()
162     bound_ll.Transform(m_to_ll)
163
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))
168
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):
174                 break
175         print(city['name'])
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'])
184
185     car_distances_to_append = sorted(car_distances_to_append, key=lambda di: di['km'])[:max_number_distances]
186
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)
190
191     car_distance_list = sorted(car_distance_list, key=lambda di: di['km'])
192     sledrun_json['car_distances'] = car_distance_list
193
194     if sledrun_json == sledrun_json_orig:
195         return
196
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)
201
202     site(
203         'edit',
204         pageid=sledrun_json_page['pageid'],
205         text=sledrun_json_str,
206         summary=f'Entfernungen zu {title} aktualisiert (dank VAO).',
207         # minor=1,
208         bot=1,
209         baserevid=sledrun_json_page['revisions'][0]['revid'],
210         nocreate=1,
211         token=site.token(),
212     )
213
214
215 def update_car_distances(ini_files: List[str]):
216     ogr.UseExceptions()
217
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')
224
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)
227
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']:
232             print(page['title'])
233             if page['title'] in ['Anzère', 'Hochhäderich (Falkenhütte)', 'Hochlitten-Moosalpe', 'Saas-Fee']:
234                 continue
235             update_sledrun(vao, db_cities, site, page['title'])
236
237
238 def main():
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)
243
244
245 if __name__ == '__main__':
246     main()