1
|
|
|
from datetime import datetime |
2
|
|
|
from enum import Enum |
3
|
|
|
import json |
4
|
|
|
import os |
5
|
|
|
import requests |
6
|
|
|
|
7
|
|
|
import ns_api |
8
|
|
|
|
9
|
|
|
from nsmaps.local_settings import USERNAME, APIKEY |
10
|
|
|
from nsmaps.logger import logger |
11
|
|
|
|
12
|
|
|
|
13
|
|
|
class StationType(Enum): |
14
|
|
|
stoptreinstation = 1 |
15
|
|
|
megastation = 2 |
16
|
|
|
knooppuntIntercitystation = 3 |
17
|
|
|
sneltreinstation = 4 |
18
|
|
|
intercitystation = 5 |
19
|
|
|
knooppuntStoptreinstation = 6 |
20
|
|
|
facultatiefStation = 7 |
21
|
|
|
knooppuntSneltreinstation = 8 |
22
|
|
|
|
23
|
|
|
|
24
|
|
|
class Station(object): |
25
|
|
|
def __init__(self, nsstation, data_dir, travel_time_min=None): |
26
|
|
|
self.nsstation = nsstation |
27
|
|
|
self.data_dir = data_dir |
28
|
|
|
self.travel_time_min = travel_time_min |
29
|
|
|
|
30
|
|
|
def get_name(self): |
31
|
|
|
return self.nsstation.names['long'] |
32
|
|
|
|
33
|
|
|
def get_code(self): |
34
|
|
|
return self.nsstation.code |
35
|
|
|
|
36
|
|
|
def get_country_code(self): |
37
|
|
|
return self.nsstation.country |
38
|
|
|
|
39
|
|
|
def get_lat(self): |
40
|
|
|
return float(self.nsstation.lat) |
41
|
|
|
|
42
|
|
|
def get_lon(self): |
43
|
|
|
return float(self.nsstation.lon) |
44
|
|
|
|
45
|
|
|
def get_travel_time_filepath(self): |
46
|
|
|
return os.path.join(self.data_dir, 'traveltimes/traveltimes_from_' + self.get_code() + '.json') |
47
|
|
|
|
48
|
|
|
def has_travel_time_data(self): |
49
|
|
|
return os.path.exists(self.get_travel_time_filepath()) |
50
|
|
|
|
51
|
|
|
def get_type(self): |
52
|
|
|
return self.nsstation.stationtype |
53
|
|
|
|
54
|
|
|
def __str__(self): |
55
|
|
|
return self.get_name() + ' (' + self.get_code() + ')' + ', travel time: ' + str(self.travel_time_min) |
56
|
|
|
|
57
|
|
|
|
58
|
|
|
class Stations(object): |
59
|
|
|
def __init__(self, data_dir, test=False): |
60
|
|
|
self.data_dir = data_dir |
61
|
|
|
self.stations = [] |
62
|
|
|
nsapi = ns_api.NSAPI(USERNAME, APIKEY) |
63
|
|
|
nsapi_stations = nsapi.get_stations() |
64
|
|
|
for i, nsapi_station in enumerate(nsapi_stations): |
65
|
|
|
if test and i > 5 and nsapi_station.code != 'UT': |
66
|
|
|
continue |
67
|
|
|
if nsapi_station.country != 'NL': |
68
|
|
|
continue |
69
|
|
|
station = Station(nsapi_station, data_dir) |
70
|
|
|
self.stations.append(station) |
71
|
|
|
|
72
|
|
|
def __iter__(self): |
73
|
|
|
return self.stations.__iter__() |
74
|
|
|
|
75
|
|
|
def __len__(self): |
76
|
|
|
return self.stations.__len__() |
77
|
|
|
|
78
|
|
|
# def from_json(self, filename): |
79
|
|
|
# stations_new = [] |
80
|
|
|
# with open(filename) as file: |
81
|
|
|
# stations = json.load(file)['stations'] |
82
|
|
|
# for station in stations: |
83
|
|
|
# self.find_station(self, station.name) |
84
|
|
|
# return stations_new |
85
|
|
|
|
86
|
|
|
def find_station(self, name): |
87
|
|
|
for station in self.stations: |
88
|
|
|
if station.get_name() == name: |
89
|
|
|
return station |
90
|
|
|
return None |
91
|
|
|
|
92
|
|
|
def travel_times_from_json(self, filename): |
93
|
|
|
with open(filename) as file: |
94
|
|
|
travel_times = json.load(file)['stations'] |
95
|
|
|
for travel_time in travel_times: |
96
|
|
|
station_name = travel_time['name'] |
97
|
|
|
station = self.find_station(station_name) |
98
|
|
|
if station: |
99
|
|
|
station.travel_time_min = int(travel_time['travel_time_min']) |
100
|
|
|
|
101
|
|
|
def update_station_data(self, filename_out): |
102
|
|
|
data = {'stations': []} |
103
|
|
|
for station in self.stations: |
104
|
|
|
# if station.country == "NL" and "Utrecht" in station.names['long']: |
105
|
|
|
travel_times_available = station.has_travel_time_data() |
106
|
|
|
contour_available = os.path.exists(os.path.join(self.data_dir, 'contours/' + station.get_code() + '.geojson')) |
107
|
|
|
data['stations'].append({'names': station.nsstation.names, |
108
|
|
|
'id': station.get_code(), |
109
|
|
|
'lon': station.get_lon(), |
110
|
|
|
'lat': station.get_lat(), |
111
|
|
|
'type': station.nsstation.stationtype, |
112
|
|
|
'travel_times_available': travel_times_available and contour_available}) |
113
|
|
|
json_data = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False) |
114
|
|
|
with open(os.path.join(self.data_dir, filename_out), 'w') as fileout: |
115
|
|
|
fileout.write(json_data) |
116
|
|
|
|
117
|
|
|
def get_stations_for_types(self, station_types): |
118
|
|
|
selected_stations = [] |
119
|
|
|
for station in self.stations: |
120
|
|
|
for station_type in station_types: |
121
|
|
|
if station.nsstation.stationtype == station_type.name: |
122
|
|
|
selected_stations.append(station) |
123
|
|
|
return selected_stations |
124
|
|
|
|
125
|
|
|
def create_traveltimes_data(self, stations_from, timestamp): |
126
|
|
|
""" timestamp format: DD-MM-YYYY hh:mm """ |
127
|
|
|
for station_from in stations_from: |
128
|
|
|
filename_out = station_from.get_travel_time_filepath() |
129
|
|
|
if os.path.exists(filename_out): |
130
|
|
|
logger.warning('File ' + filename_out + ' already exists. Will not overwrite. Return.') |
131
|
|
|
continue |
132
|
|
|
json_data = self.create_trip_data_from_station(station_from, timestamp) |
133
|
|
|
with open(filename_out, 'w') as fileout: |
134
|
|
|
fileout.write(json_data) |
135
|
|
|
|
136
|
|
|
def get_station_code(self, station_name): |
137
|
|
|
for station in self.stations: |
138
|
|
|
if station.get_name() == station_name: |
139
|
|
|
return station.get_code() |
140
|
|
|
return None |
141
|
|
|
|
142
|
|
|
def create_trip_data_from_station(self, station_from, timestamp): |
143
|
|
|
""" timestamp format: DD-MM-YYYY hh:mm """ |
144
|
|
|
via = "" |
145
|
|
|
data = {'stations': []} |
146
|
|
|
data['stations'].append({'name': station_from.get_name(), |
147
|
|
|
'id': station_from.get_code(), |
148
|
|
|
'travel_time_min': 0, |
149
|
|
|
'travel_time_planned': "0:00"}) |
150
|
|
|
nsapi = ns_api.NSAPI(USERNAME, APIKEY) |
151
|
|
|
for station in self.stations: |
152
|
|
|
if station.get_code() == station_from.get_code(): |
153
|
|
|
continue |
154
|
|
|
trips = [] |
155
|
|
|
try: |
156
|
|
|
trips = nsapi.get_trips(timestamp, station_from.get_code(), via, station.get_code()) |
157
|
|
|
except TypeError as error: |
158
|
|
|
# this is a bug in ns-api, should return empty trips in case there are no results |
159
|
|
|
logger.error('Error while trying to get trips for destination: ' + station.get_name() + ', from: ' + station_from.get_name()) |
160
|
|
|
continue |
161
|
|
|
except requests.exceptions.HTTPError as error: |
162
|
|
|
# 500: Internal Server Error does always happen for some stations (example are Eijs-Wittem and Kerkrade-West) |
163
|
|
|
logger.error('HTTP Error while trying to get trips for destination: ' + station.get_name() + ', from: ' + station_from.get_name()) |
164
|
|
|
continue |
165
|
|
|
|
166
|
|
|
if not trips: |
167
|
|
|
continue |
168
|
|
|
|
169
|
|
|
shortest_trip = trips[0] |
170
|
|
|
for trip in trips: |
171
|
|
|
travel_time = datetime.strptime(trip.travel_time_planned, "%H:%M").time() |
172
|
|
|
trip.travel_time_min = travel_time.hour * 60 + travel_time.minute |
173
|
|
|
if trip.travel_time_min < shortest_trip.travel_time_min: |
174
|
|
|
shortest_trip = trip |
175
|
|
|
|
176
|
|
|
logger.info(shortest_trip.departure + ' - ' + shortest_trip.destination) |
177
|
|
|
data['stations'].append({'name': shortest_trip.destination, |
178
|
|
|
'id': self.get_station_code(shortest_trip.destination), |
179
|
|
|
'travel_time_min': shortest_trip.travel_time_min, |
180
|
|
|
'travel_time_planned': shortest_trip.travel_time_planned}) |
181
|
|
|
# time.sleep(0.3) # balance load on the NS server |
182
|
|
|
json_data = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False) |
183
|
|
|
return json_data |
184
|
|
|
|
185
|
|
|
def get_missing_destinations(self, filename_json): |
186
|
|
|
self.travel_times_from_json(filename_json) |
187
|
|
|
missing_stations = [] |
188
|
|
|
for station in self.stations: |
189
|
|
|
if station.travel_time_min is None: |
190
|
|
|
missing_stations.append(station) |
191
|
|
|
return missing_stations |
192
|
|
|
|
193
|
|
|
def recreate_missing_destinations(self, departure_timestamp, dry_run=False): |
194
|
|
|
ignore_station_ids = ['HRY', 'WTM', 'KRW', 'VMW', 'RTST', 'WIJ', 'SPV', 'SPH'] |
195
|
|
|
for station in self.stations: |
196
|
|
|
if not station.has_travel_time_data(): |
197
|
|
|
continue |
198
|
|
|
stations_missing = self.get_missing_destinations(station.get_travel_time_filepath()) |
199
|
|
|
stations_missing_filtered = [] |
200
|
|
|
for station_missing in stations_missing: |
201
|
|
|
if station_missing.get_code() not in ignore_station_ids: |
202
|
|
|
stations_missing_filtered.append(stations_missing) |
203
|
|
|
logger.info(station.get_name() + ' has missing station: ' + station_missing.get_name()) |
204
|
|
|
if stations_missing_filtered and not dry_run: |
205
|
|
|
json_data = self.create_trip_data_from_station(station, departure_timestamp) |
206
|
|
|
with open(station.get_travel_time_filepath(), 'w') as fileout: |
207
|
|
|
fileout.write(json_data) |
208
|
|
|
else: |
209
|
|
|
logger.info('No missing destinations for ' + station.get_name() + ' with ' + str(len(ignore_station_ids)) + ' ignored.') |
210
|
|
|
|
211
|
|
|
|