├── requirements.txt ├── .gitignore ├── LICENSE ├── README.md └── dvb.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.5.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea 3 | dvb.pyc 4 | env/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kilian Koeltzsch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## dvbpy 2 | 3 | An unofficial python module giving you a few options to query a collection of publicly accessible API methods for Dresden's public transport system. 4 | 5 | In case you're looking for something like this for node.js, check out [dvbjs](https://github.com/kiliankoe/dvbjs). 6 | 7 | dvbpy is not available on PyPI for the time being. Please download it yourself and import it to get started. 8 | 9 | ```python 10 | import dvb 11 | ``` 12 | 13 | ### Requirements 14 | 15 | dvbpy needs __requests__ for HTTP-Communication and __pyproj__ for geocoordinate transformations 16 | * `pip install requests` 17 | * `pip install pyproj` 18 | 19 | 20 | ### Monitor a single stop 21 | 22 | Monitor a single stop to see every bus or tram leaving this stop after the specified time offset. 23 | 24 | ```python 25 | import dvb 26 | 27 | stop = 'Helmholtzstraße' 28 | time_offset = 0 # how many minutes in the future, 0 for now 29 | num_results = 2 30 | city = 'Dresden' 31 | 32 | dvb.monitor(stop, time_offset, num_results, city) 33 | ``` 34 | 35 | ```python 36 | [{ 37 | 'line': '85', 38 | 'direction': 'Striesen', 39 | 'arrival': 5 40 | }, 41 | { 42 | 'line': '85', 43 | 'direction': 'Löbtau Süd', 44 | 'arrival': 7 45 | }] 46 | ``` 47 | 48 | You can also call `monitor()` without city, num_results or time_offset. City will default to Dresden. 49 | 50 | 51 | ### Find routes 52 | 53 | Query the server for possible routes from one stop to another. Returns multiple possible trips, the bus-/tramlines to be taken, the single stops, their arrival and departure times and their GPS coordinates. 54 | 55 | ```python 56 | import dvb 57 | import time 58 | 59 | origin = 'Zellescher Weg' 60 | city_origin = 'Dresden' 61 | destination = 'Postplatz' 62 | city_destination = 'Dresden' 63 | time = int(time.time()) # a unix timestamp is wanted here 64 | deparr = 'dep' # set to 'arr' for arrival time, 'dep' for departure time 65 | 66 | dvb.route(origin, destination, city_origin, city_destination, time, deparr) 67 | ``` 68 | 69 | ```python 70 | { 71 | 'trips': [{ 72 | 'interchange': 0, 73 | 'nodes': [{ 74 | 'line': '11', 75 | 'mode': 'Straßenbahn', 76 | 'direction': 'Dresden Bühlau Ullersdorfer Platz', 77 | 'path': [ 78 | [13.745754, 51.02816], 79 | [13.745848, 51.028393], 80 | ... 81 | ], 82 | 'departure': { 83 | 'time': '18:01', 84 | 'stop': 'Zellescher Weg', 85 | 'coords': '13745754,51028160' 86 | }, 87 | 'arrival': { 88 | 'time': '18:14', 89 | 'stop': 'Postplatz', 90 | 'coords': '13733717,51050544' 91 | } 92 | }], 93 | 'duration': '00:13', 94 | 'departure': '18:01', 95 | 'arrival': '18:14' 96 | }, 97 | ... 98 | }], 99 | 'origin': 'Dresden, Zellescher Weg', 100 | 'destination': 'Dresden, Postplatz' 101 | } 102 | ``` 103 | 104 | Everything besides origin and destination is optional and only needs to be included if necessary. City for origin and destination defaults to Dresden, time to now and is handled as the departure time. 105 | 106 | The path property contains a list consisting of all the coordinates describing the path of this node. Useful for example if you want to draw it on a map. 107 | 108 | 109 | ### Find stops by name 110 | 111 | Search for a single stop in the network of the DVB. 112 | 113 | ```python 114 | import dvb 115 | 116 | dvb.find('zellesch') 117 | ``` 118 | 119 | ```python 120 | [{ 121 | 'name': 'Zellescher Weg', 122 | 'city': 'Dresden', 123 | 'coords': [51.028366, 13.745847] 124 | }] 125 | ``` 126 | 127 | The fields `city` and `coords` are optional as they are not available for every stop. So don't forget to check for their existence first. 128 | 129 | ```python 130 | [stop for stop in dvb.find('Post') if 'city' in stop if stop['city'] == 'Dresden'] 131 | ``` 132 | 133 | 134 | ### Find other POIs with coordinates 135 | 136 | Search for all kinds of POIs inside a given square. 137 | ```python 138 | import dvb 139 | 140 | southwest_lat = 51.04120 141 | southwest_lng = 13.70106 142 | northeast_lat = 51.04615 143 | northeast_lng = 13.71368 144 | 145 | pintypes = 'stop' 146 | # can be poi, platform, rentabike, ticketmachine, parkandride, carsharing or stop 147 | 148 | dvb.pins(southwest_lat, southwest_lng, northeast_lat, northeast_lng, pintypes) 149 | ``` 150 | 151 | `pintypes` defaults to 'stop' if no other input is given. 152 | 153 | ```python 154 | [ 155 | { 156 | "connections":"1:7~8~9~10~11~12", 157 | "coords":{ 158 | "lat":51.04373256804444, 159 | "lng":13.70625638110702 160 | }, 161 | "id":33000143, 162 | "name":"Saxoniastraße" 163 | }, 164 | { 165 | "connections":"2:61~90", 166 | "coords":{ 167 | "lat":51.04159705545878, 168 | "lng":13.7053650905211 169 | }, 170 | "id":33000700, 171 | "name":"Ebertplatz" 172 | }, 173 | { 174 | "connections":"1:6~7~8~9~10~11~12#2:61~63~90~A#3:333", 175 | "coords":{ 176 | "lat":51.04372841952444, 177 | "lng":13.703461228676069 178 | }, 179 | "id":33000144, 180 | "name":"Tharandter Straße" 181 | }, ... 182 | ] 183 | ``` 184 | 185 | 186 | ### Look up coordinates for POI 187 | 188 | Find the coordinates for a given POI id. 189 | ```python 190 | import dvb 191 | 192 | dvb.poi_coords(33000755) 193 | ``` 194 | 195 | ```python 196 | {'lat': 51.018745307424005, 'lng': 13.758700156062707} 197 | ``` 198 | 199 | 200 | ### Address for coordinates - WIP 201 | 202 | Look up the address for a given set of coordinates. 203 | ```python 204 | import dvb 205 | 206 | lat = 51.04373 207 | lng = 13.70320 208 | 209 | dvb.address(lat, lng) 210 | ``` 211 | 212 | ```python 213 | { 214 | 'city': u'Dresden', 215 | 'address': u'Kesselsdorfer Straße 1' 216 | } 217 | ``` 218 | 219 | 220 | ### Other stuff 221 | 222 | Stop names in queries are very forgiving. As long as the server sees it as a unique hit, it'll work. 'Helmholtzstraße' finds the same data as 'helmholtzstrasse', 'Nürnberger Platz' = 'nuernbergerplatz' etc. 223 | 224 | One last note, be sure not to run whatever it is you're building from inside the network of the TU Dresden. Calls to `dvb.route()` and `dvb.find()` will time out. This is unfortunately expected behavior as API calls from these IP ranges are blocked. 225 | -------------------------------------------------------------------------------- /dvb.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import pyproj 4 | from datetime import datetime 5 | 6 | 7 | def monitor(stop, offset=0, limit=10, city='Dresden'): 8 | # VVO Online Monitor 9 | # (GET http://widgets.vvo-online.de/abfahrtsmonitor/Abfahrten.do) 10 | try: 11 | r = requests.get( 12 | url='http://widgets.vvo-online.de/abfahrtsmonitor/Abfahrten.do', 13 | params={ 14 | 'ort': city, 15 | 'hst': stop, 16 | 'vz': offset, 17 | 'lim': limit, 18 | }, 19 | ) 20 | if r.status_code == 200: 21 | response = json.loads(r.content.decode('utf-8')) 22 | else: 23 | print('Failed to access VVO monitor. HTTP Error ' + r.status_code) 24 | response = None 25 | except requests.RequestException as e: 26 | print('Failed to access VVO monitor. Request Exception ' + e) 27 | response = None 28 | 29 | if response is None: 30 | return response 31 | else: 32 | return [ 33 | { 34 | 'line': line, 35 | 'direction': direction, 36 | 'arrival': int(arrival) 37 | } for line, direction, arrival in response 38 | ] 39 | 40 | 41 | def process_single_trip(single_trip): 42 | def process_leg(leg): 43 | path = [convert_coords(a) for a in leg['path'].split(' ')] if 'path' in leg else None 44 | 45 | departure = { 46 | 'stop': leg['points'][0]['nameWO'], 47 | 'time': leg['points'][0]['dateTime']['time'], 48 | 'coords': leg['points'][0]['ref']['coords'] 49 | } 50 | 51 | arrival = { 52 | 'stop': leg['points'][1]['nameWO'], 53 | 'time': leg['points'][1]['dateTime']['time'], 54 | 'coords': leg['points'][1]['ref']['coords'] 55 | } 56 | 57 | return { 58 | 'mode': leg['mode']['product'], 59 | 'line': leg['mode']['number'], 60 | 'direction': leg['mode']['destination'], 61 | 'departure': departure, 62 | 'arrival': arrival, 63 | 'path': path 64 | } 65 | 66 | return { 67 | 'departure': single_trip['legs'][0]['points'][0]['dateTime']['time'], 68 | 'arrival': single_trip['legs'][-1]['points'][-1]['dateTime']['time'], 69 | 'duration': single_trip['duration'], 70 | 'interchange': int(single_trip['interchange']), 71 | 'nodes': [process_leg(leg) for leg in single_trip['legs']] 72 | } 73 | 74 | 75 | def route(origin, destination, city_origin='Dresden', city_destination='Dresden', time=0, deparr='dep', eduroam=False): 76 | # VVO Online EFA TripRequest 77 | # (GET http://efa.vvo-online.de:8080/dvb/XML_TRIP_REQUEST2) 78 | 79 | time = datetime.now() if time == 0 else datetime.fromtimestamp(int(datetime)) 80 | 81 | url = 'http://efa.faplino.de/dvb/XML_TRIP_REQUEST2' if eduroam \ 82 | else 'http://efa.vvo-online.de:8080/dvb/XML_TRIP_REQUEST2' 83 | 84 | try: 85 | r = requests.get( 86 | url=url, 87 | params={ 88 | 'sessionID': '0', 89 | 'requestID': '0', 90 | 'language': 'de', 91 | 'execInst': 'normal', 92 | 'command': '', 93 | 'ptOptionsActive': '-1', 94 | 'itOptionsActive': '', 95 | 'itDateDay': time.day, 96 | 'itDateMonth': time.month, 97 | 'itDateYear': time.year, 98 | 'place_origin': city_origin, 99 | 'placeState_origin': 'empty', 100 | 'type_origin': 'stop', 101 | 'name_origin': origin, 102 | 'nameState_origin': 'empty', 103 | 'place_destination': city_destination, 104 | 'placeState_destination': 'empty', 105 | 'type_destination': 'stop', 106 | 'name_destination': destination, 107 | 'nameState_destination': 'empty', 108 | 'itdTripDateTimeDepArr': deparr, 109 | 'itdTimeHour': time.hour, 110 | 'idtTimeMinute': time.minute, 111 | 'outputFormat': 'JSON', 112 | 'coordOutputFormat': 'WGS84', 113 | 'coordOutputFormatTail': '0', 114 | }, 115 | timeout=10 116 | ) 117 | if r.status_code == 200: 118 | response = json.loads(r.content.decode('utf-8')) 119 | else: 120 | print('Failed to access VVO TripRequest. HTTP Error ' + r.status_code) 121 | response = None 122 | except requests.Timeout: 123 | print('Failed to access VVO TripRequest. Connection timed out. Are you connected to eduroam?') 124 | response = None 125 | except requests.RequestException as e: 126 | print('Failed to access VVO TripRequest. Request Exception ' + e) 127 | response = None 128 | 129 | if response is None: 130 | return response 131 | else: 132 | return { 133 | 'origin': response['origin']['points']['point']['name'], 134 | 'destination': response['destination']['points']['point']['name'], 135 | 'trips': [ 136 | process_single_trip(single_trip) for single_trip in response['trips'] 137 | ] 138 | } 139 | 140 | 141 | def find(search, eduroam=False): 142 | # VVO Online EFA Stopfinder 143 | # (GET http://efa.vvo-online.de:8080/dvb/XML_STOPFINDER_REQUEST) 144 | url = 'http://efa.faplino.de/dvb/XML_STOPFINDER_REQUEST' if eduroam \ 145 | else 'http://efa.vvo-online.de:8080/dvb/XML_STOPFINDER_REQUEST' 146 | 147 | try: 148 | r = requests.get( 149 | url=url, 150 | params={ 151 | 'locationServerActive': '1', 152 | 'outputFormat': 'JSON', 153 | 'type_sf': 'any', 154 | 'name_sf': search, 155 | 'coordOutputFormat': 'WGS84', 156 | 'coordOutputFormatTail': '0', 157 | }, 158 | timeout=10 159 | ) 160 | if r.status_code == 200: 161 | response = json.loads(r.content.decode('utf-8')) 162 | else: 163 | print('Failed to access VVO StopFinder. HTTP Error ' + r.status_code) 164 | response = None 165 | except requests.Timeout: 166 | print('Failed to access VVO StopFinder. Connection timed out. Are you connected to eduroam?') 167 | response = None 168 | except requests.RequestException as e: 169 | print('Failed to access VVO StopFinder. Request Exception ' + e) 170 | response = None 171 | 172 | if response is None: 173 | return response 174 | else: 175 | points = response['stopFinder']['points'] 176 | return [ 177 | # single result 178 | find_return_results(points['point']) 179 | ] if 'point' in points else [ 180 | # multiple results 181 | find_return_results(stop) 182 | for stop in points 183 | ] 184 | 185 | 186 | def find_return_results(stop): 187 | if 'object' in stop and 'coords' in stop['ref']: 188 | # city and coords 189 | return { 190 | 'name': stop['object'], 191 | 'city': stop['posttown'], 192 | 'coords': convert_coords(stop['ref']['coords']) 193 | } 194 | elif 'object' in stop: 195 | # only city, no coords 196 | return { 197 | 'name': stop['object'], 198 | 'city': stop['posttown'] 199 | } 200 | elif 'coords' in stop['ref']: 201 | # only coords, no city 202 | return { 203 | 'name': stop['name'], 204 | 'coords': convert_coords(stop['ref']['coords']) 205 | } 206 | else: 207 | # neither city or coords 208 | return { 209 | 'name': stop['name'] 210 | } 211 | 212 | 213 | def convert_coords(coords): 214 | # for i in coords.split(','): 215 | # yield int(i) / 1000000 216 | coords = coords.split(',') 217 | for i in range(len(coords)): 218 | coords[i] = int(coords[i]) 219 | coords[i] /= 1000000 220 | return coords 221 | 222 | 223 | def pins(swlat, swlng, nelat, nelng, pintypes='stop'): 224 | # DVB Map Pins (GET https://www.dvb.de/apps/map/pins) 225 | try: 226 | swlat, swlng = wgs_to_gk4(swlat, swlng) 227 | nelat, nelng = wgs_to_gk4(nelat, nelng) 228 | r = requests.get( 229 | url='https://www.dvb.de/apps/map/pins', 230 | params={ 231 | 'showlines': 'true', 232 | 'swlat': swlat, 233 | 'swlng': swlng, 234 | 'nelat': nelat, 235 | 'nelng': nelng, 236 | 'pintypes': pintypes, 237 | }, 238 | ) 239 | if r.status_code == 200: 240 | response = json.loads(r.content.decode('utf-8')) 241 | else: 242 | print('Failed to access DVB map pins app. HTTP Error ' + r.status_code) 243 | response = None 244 | except requests.RequestException as e: 245 | print('Failed to access DVB map pins app. Request Exception ' + e) 246 | response = None 247 | 248 | if response is None: 249 | return response 250 | else: 251 | return [pins_return_results(line, pintypes) for line in response] 252 | 253 | 254 | def pins_return_results(line, pintypes): 255 | if pintypes == 'stop': 256 | return { 257 | 'id': int(line.split('|||')[0]), 258 | 'name': line.split('||')[1].split('|')[1], 259 | 'coords': pincoords_to_object( 260 | int(line.split('||')[1].split('|')[2]), 261 | int(line.split('||')[1].split('|')[3]) 262 | ), 263 | 'connections': line.split('||')[2] 264 | } 265 | elif pintypes == 'platform': 266 | return { 267 | 'name': line.split('||')[1].split('|')[0], 268 | 'coords': pincoords_to_object( 269 | int(line.split('||')[1].split('|')[1]), 270 | int(line.split('||')[1].split('|')[2]) 271 | ), 272 | '?': line.split('||')[1].split('|')[3] 273 | } 274 | elif pintypes == 'poi' or pintypes == 'rentabike' or pintypes == 'ticketmachine' \ 275 | or pintypes == 'carsharing' or pintypes == 'parkandride': 276 | return { 277 | 'id': ':'.join(line.split('||')[0].split(':')[:3]), 278 | 'name': line.split('||')[1].split('|')[0], 279 | 'coords': pincoords_to_object( 280 | int(line.split('||')[1].split('|')[1]), 281 | int(line.split('||')[1].split('|')[2]) 282 | ) 283 | } 284 | 285 | 286 | def poi_coords(poi_id): 287 | # DVB Map Coordinates (GET https://www.dvb.de/apps/map/coordinates) 288 | try: 289 | r = requests.get( 290 | url='https://www.dvb.de/apps/map/coordinates', 291 | params={ 292 | 'id': poi_id, 293 | }, 294 | ) 295 | if r.status_code == 200: 296 | response = json.loads(r.content.decode('utf-8')) 297 | else: 298 | print('Failed to access DVB map coordinates app. HTTP Error ' + r.status_code) 299 | response = None 300 | except requests.RequestException as e: 301 | print('Failed to access DVB map coordinates app. Request Exception ' + e) 302 | response = None 303 | 304 | if response is None: 305 | return response 306 | else: 307 | coords = [int(i) for i in response.split('|')] 308 | lat, lng = gk4_to_wgs(coords[0], coords[1]) 309 | return { 310 | 'lat': lat, 311 | 'lng': lng 312 | } 313 | 314 | 315 | def address(lat, lng): 316 | # DVB Map Address (GET https://www.dvb.de/apps/map/address) 317 | try: 318 | lat, lng = wgs_to_gk4(lat, lng) 319 | r = requests.get( 320 | url='https://www.dvb.de/apps/map/address', 321 | params={ 322 | 'lat': lat, 323 | 'lng': lng, 324 | }, 325 | ) 326 | if r.status_code == 200: 327 | response = json.loads(r.content.decode('utf-8')) 328 | else: 329 | print('Failed to access DVB map address app. HTTP Error ' + r.status_code) 330 | response = None 331 | except requests.RequestException as e: 332 | print('Failed to access DVB map address app. Request Exception ' + e) 333 | response = None 334 | 335 | if response is None: 336 | return response 337 | else: 338 | return process_address(response) 339 | 340 | 341 | def process_address(line): 342 | try: 343 | return { 344 | 'city': line.split('|')[0], 345 | 'address': line.split('|')[1] 346 | } 347 | except Exception as e: 348 | print('Address not found. Error: ' + e.__str__()) 349 | return None 350 | 351 | def wgs_to_gk4(lat, lng): 352 | #transforms coordinates from WGS84 to Gauss-Kruger zone 4 353 | wgs = pyproj.Proj(init='epsg:4326') 354 | gk4 = pyproj.Proj(init='epsg:5678') 355 | lngOut, latOut = pyproj.transform(wgs,gk4,lng,lat) 356 | return int(latOut), int(lngOut) 357 | 358 | def gk4_to_wgs(lat, lng): 359 | #transforms coordinates from Gauss-Kruger zone 4 to WGS84 360 | wgs = pyproj.Proj(init='epsg:4326') 361 | gk4 = pyproj.Proj(init='epsg:5678') 362 | lngOut, latOut = pyproj.transform(gk4,wgs,lng,lat) 363 | return latOut, lngOut 364 | 365 | def pincoords_to_object (lat, lng): 366 | lat, lng = gk4_to_wgs(lat, lng) 367 | return { 368 | 'lat': lat, 369 | 'lng': lng 370 | } 371 | 372 | --------------------------------------------------------------------------------