Interpolate stop times for non timing point stops
[bus.git] / busui / schedule_viewer.py
blob:a/busui/schedule_viewer.py -> blob:b/busui/schedule_viewer.py
#!/usr/bin/python2.5 #!/usr/bin/python2.5
   
# Copyright (C) 2007 Google Inc. # Copyright (C) 2007 Google Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
   
""" """
An example application that uses the transitfeed module. An example application that uses the transitfeed module.
   
You must provide a Google Maps API key. You must provide a Google Maps API key.
""" """
   
   
import BaseHTTPServer, sys, urlparse import BaseHTTPServer, sys, urlparse
import bisect import bisect
from gtfsscheduleviewer.marey_graph import MareyGraph from gtfsscheduleviewer.marey_graph import MareyGraph
import gtfsscheduleviewer import gtfsscheduleviewer
import mimetypes import mimetypes
import os.path import os.path
import re import re
import signal import signal
import simplejson import simplejson
import socket import socket
import time import time
import transitfeed import transitfeed
from transitfeed import util from transitfeed import util
import urllib import urllib
   
   
# By default Windows kills Python with Ctrl+Break. Instead make Ctrl+Break # By default Windows kills Python with Ctrl+Break. Instead make Ctrl+Break
# raise a KeyboardInterrupt. # raise a KeyboardInterrupt.
if hasattr(signal, 'SIGBREAK'): if hasattr(signal, 'SIGBREAK'):
signal.signal(signal.SIGBREAK, signal.default_int_handler) signal.signal(signal.SIGBREAK, signal.default_int_handler)
   
   
mimetypes.add_type('text/plain', '.vbs') mimetypes.add_type('text/plain', '.vbs')
   
   
class ResultEncoder(simplejson.JSONEncoder): class ResultEncoder(simplejson.JSONEncoder):
def default(self, obj): def default(self, obj):
try: try:
iterable = iter(obj) iterable = iter(obj)
except TypeError: except TypeError:
pass pass
else: else:
return list(iterable) return list(iterable)
return simplejson.JSONEncoder.default(self, obj) return simplejson.JSONEncoder.default(self, obj)
   
# Code taken from # Code taken from
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/425210/index_txt # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/425210/index_txt
# An alternate approach is shown at # An alternate approach is shown at
# http://mail.python.org/pipermail/python-list/2003-July/212751.html # http://mail.python.org/pipermail/python-list/2003-July/212751.html
# but it requires multiple threads. A sqlite object can only be used from one # but it requires multiple threads. A sqlite object can only be used from one
# thread. # thread.
class StoppableHTTPServer(BaseHTTPServer.HTTPServer): class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
def server_bind(self): def server_bind(self):
BaseHTTPServer.HTTPServer.server_bind(self) BaseHTTPServer.HTTPServer.server_bind(self)
self.socket.settimeout(1) self.socket.settimeout(1)
self._run = True self._run = True
   
def get_request(self): def get_request(self):
while self._run: while self._run:
try: try:
sock, addr = self.socket.accept() sock, addr = self.socket.accept()
sock.settimeout(None) sock.settimeout(None)
return (sock, addr) return (sock, addr)
except socket.timeout: except socket.timeout:
pass pass
   
def stop(self): def stop(self):
self._run = False self._run = False
   
def serve(self): def serve(self):
while self._run: while self._run:
self.handle_request() self.handle_request()
   
   
def StopToTuple(stop): def StopToTuple(stop):
"""Return tuple as expected by javascript function addStopMarkerFromList""" """Return tuple as expected by javascript function addStopMarkerFromList"""
return (stop.stop_id, stop.stop_name, float(stop.stop_lat), return (stop.stop_id, stop.stop_name, float(stop.stop_lat),
float(stop.stop_lon), stop.location_type) float(stop.stop_lon), stop.location_type, stop.stop_code)
  def StopCodeToTuple(stop, code):
  return (stop.stop_id, stop.stop_name, float(stop.stop_lat),
  float(stop.stop_lon), stop.location_type, code)
   
   
class ScheduleRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): class ScheduleRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
scheme, host, path, x, params, fragment = urlparse.urlparse(self.path) scheme, host, path, x, params, fragment = urlparse.urlparse(self.path)
parsed_params = {} parsed_params = {}
for k in params.split('&'): for k in params.split('&'):
k = urllib.unquote(k) k = urllib.unquote(k)
if '=' in k: if '=' in k:
k, v = k.split('=', 1) k, v = k.split('=', 1)
parsed_params[k] = unicode(v, 'utf8') parsed_params[k] = unicode(v, 'utf8')
else: else:
parsed_params[k] = '' parsed_params[k] = ''
   
if path == '/': if path == '/':
return self.handle_GET_home() return self.handle_GET_home()
   
m = re.match(r'/json/([a-z]{1,64})', path) m = re.match(r'/json/([a-z]{1,64})', path)
if m: if m:
handler_name = 'handle_json_GET_%s' % m.group(1) handler_name = 'handle_json_GET_%s' % m.group(1)
handler = getattr(self, handler_name, None) handler = getattr(self, handler_name, None)
if callable(handler): if callable(handler):
return self.handle_json_wrapper_GET(handler, parsed_params) return self.handle_json_wrapper_GET(handler, parsed_params)
   
# Restrict allowable file names to prevent relative path attacks etc # Restrict allowable file names to prevent relative path attacks etc
m = re.match(r'/file/([a-z0-9_-]{1,64}\.?[a-z0-9_-]{1,64})$', path) m = re.match(r'/file/([a-z0-9_-]{1,64}\.?[a-z0-9_-]{1,64})$', path)
if m and m.group(1): if m and m.group(1):
try: try:
f, mime_type = self.OpenFile(m.group(1)) f, mime_type = self.OpenFile(m.group(1))
return self.handle_static_file_GET(f, mime_type) return self.handle_static_file_GET(f, mime_type)
except IOError, e: except IOError, e:
print "Error: unable to open %s" % m.group(1) print "Error: unable to open %s" % m.group(1)
# Ignore and treat as 404 # Ignore and treat as 404
   
m = re.match(r'/([a-z]{1,64})', path) m = re.match(r'/([a-z]{1,64})', path)
if m: if m:
handler_name = 'handle_GET_%s' % m.group(1) handler_name = 'handle_GET_%s' % m.group(1)
handler = getattr(self, handler_name, None) handler = getattr(self, handler_name, None)
if callable(handler): if callable(handler):
return handler(parsed_params) return handler(parsed_params)
   
return self.handle_GET_default(parsed_params, path) return self.handle_GET_default(parsed_params, path)
   
def OpenFile(self, filename): def OpenFile(self, filename):
"""Try to open filename in the static files directory of this server. """Try to open filename in the static files directory of this server.
Return a tuple (file object, string mime_type) or raise an exception.""" Return a tuple (file object, string mime_type) or raise an exception."""
(mime_type, encoding) = mimetypes.guess_type(filename) (mime_type, encoding) = mimetypes.guess_type(filename)
assert mime_type assert mime_type
# A crude guess of when we should use binary mode. Without it non-unix # A crude guess of when we should use binary mode. Without it non-unix
# platforms may corrupt binary files. # platforms may corrupt binary files.
if mime_type.startswith('text/'): if mime_type.startswith('text/'):
mode = 'r' mode = 'r'
else: else:
mode = 'rb' mode = 'rb'
return open(os.path.join(self.server.file_dir, filename), mode), mime_type return open(os.path.join(self.server.file_dir, filename), mode), mime_type
   
def handle_GET_default(self, parsed_params, path): def handle_GET_default(self, parsed_params, path):
self.send_error(404) self.send_error(404)
   
def handle_static_file_GET(self, fh, mime_type): def handle_static_file_GET(self, fh, mime_type):
content = fh.read() content = fh.read()
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', mime_type) self.send_header('Content-Type', mime_type)
self.send_header('Content-Length', str(len(content))) self.send_header('Content-Length', str(len(content)))
self.end_headers() self.end_headers()
self.wfile.write(content) self.wfile.write(content)
   
def AllowEditMode(self): def AllowEditMode(self):
return False return False
   
def handle_GET_home(self): def handle_GET_home(self):
schedule = self.server.schedule schedule = self.server.schedule
(min_lat, min_lon, max_lat, max_lon) = schedule.GetStopBoundingBox() (min_lat, min_lon, max_lat, max_lon) = schedule.GetStopBoundingBox()
forbid_editing = ('true', 'false')[self.AllowEditMode()] forbid_editing = ('true', 'false')[self.AllowEditMode()]
   
agency = ', '.join(a.agency_name for a in schedule.GetAgencyList()).encode('utf-8') agency = ', '.join(a.agency_name for a in schedule.GetAgencyList()).encode('utf-8')
   
key = self.server.key key = self.server.key
host = self.server.host host = self.server.host
   
# A very simple template system. For a fixed set of values replace [xxx] # A very simple template system. For a fixed set of values replace [xxx]
# with the value of local variable xxx # with the value of local variable xxx
f, _ = self.OpenFile('index.html') f, _ = self.OpenFile('index.html')
content = f.read() content = f.read()
for v in ('agency', 'min_lat', 'min_lon', 'max_lat', 'max_lon', 'key', for v in ('agency', 'min_lat', 'min_lon', 'max_lat', 'max_lon', 'key',
'host', 'forbid_editing'): 'host', 'forbid_editing'):
content = content.replace('[%s]' % v, str(locals()[v])) content = content.replace('[%s]' % v, str(locals()[v]))
   
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'text/html') self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', str(len(content))) self.send_header('Content-Length', str(len(content)))
self.end_headers() self.end_headers()
self.wfile.write(content) self.wfile.write(content)
   
def handle_json_GET_routepatterns(self, params): def handle_json_GET_routepatterns(self, params):
"""Given a route_id generate a list of patterns of the route. For each """Given a route_id generate a list of patterns of the route. For each
pattern include some basic information and a few sample trips.""" pattern include some basic information and a few sample trips."""
schedule = self.server.schedule schedule = self.server.schedule
route = schedule.GetRoute(params.get('route', None)) route = schedule.GetRoute(params.get('route', None))
if not route: if not route:
self.send_error(404) self.send_error(404)
return return
time = int(params.get('time', 0)) time = int(params.get('time', 0))
sample_size = 10 # For each pattern return the start time for this many trips sample_size = 10 # For each pattern return the start time for this many trips
   
pattern_id_trip_dict = route.GetPatternIdTripDict() pattern_id_trip_dict = route.GetPatternIdTripDict()
patterns = [] patterns = []
   
for pattern_id, trips in pattern_id_trip_dict.items(): for pattern_id, trips in pattern_id_trip_dict.items():
time_stops = trips[0].GetTimeStops() time_stops = trips[0].GetTimeStops()
if not time_stops: if not time_stops:
continue continue
has_non_zero_trip_type = False; has_non_zero_trip_type = False;
for trip in trips: for trip in trips:
if trip['trip_type'] and trip['trip_type'] != '0': if trip['trip_type'] and trip['trip_type'] != '0':
has_non_zero_trip_type = True has_non_zero_trip_type = True
name = u'%s to %s, %d stops' % (time_stops[0][2].stop_name, time_stops[-1][2].stop_name, len(time_stops)) name = u'%s to %s, %d stops' % (time_stops[0][2].stop_name, time_stops[-1][2].stop_name, len(time_stops))
transitfeed.SortListOfTripByTime(trips) transitfeed.SortListOfTripByTime(trips)
   
num_trips = len(trips) num_trips = len(trips)
if num_trips <= sample_size: if num_trips <= sample_size:
start_sample_index = 0 start_sample_index = 0
num_after_sample = 0 num_after_sample = 0
else: else:
# Will return sample_size trips that start after the 'time' param. # Will return sample_size trips that start after the 'time' param.
   
# Linear search because I couldn't find a built-in way to do a binary # Linear search because I couldn't find a built-in way to do a binary
# search with a custom key. # search with a custom key.
start_sample_index = len(trips) start_sample_index = len(trips)
for i, trip in enumerate(trips): for i, trip in enumerate(trips):
if trip.GetStartTime() >= time: if trip.GetStartTime() >= time:
start_sample_index = i start_sample_index = i
break break
   
num_after_sample = num_trips - (start_sample_index + sample_size) num_after_sample = num_trips - (start_sample_index + sample_size)
if num_after_sample < 0: if num_after_sample < 0:
# Less than sample_size trips start after 'time' so return all the # Less than sample_size trips start after 'time' so return all the
# last sample_size trips. # last sample_size trips.
num_after_sample = 0 num_after_sample = 0
start_sample_index = num_trips - sample_size start_sample_index = num_trips - sample_size
   
sample = [] sample = []
for t in trips[start_sample_index:start_sample_index + sample_size]: for t in trips[start_sample_index:start_sample_index + sample_size]:
sample.append( (t.GetStartTime(), t.trip_id) ) sample.append( (t.GetStartTime(), t.trip_id) )
   
patterns.append((name, pattern_id, start_sample_index, sample, patterns.append((name, pattern_id, start_sample_index, sample,
num_after_sample, (0,1)[has_non_zero_trip_type])) num_after_sample, (0,1)[has_non_zero_trip_type]))
   
patterns.sort() patterns.sort()
return patterns return patterns
   
def handle_json_wrapper_GET(self, handler, parsed_params): def handle_json_wrapper_GET(self, handler, parsed_params):
"""Call handler and output the return value in JSON.""" """Call handler and output the return value in JSON."""
schedule = self.server.schedule schedule = self.server.schedule
result = handler(parsed_params) result = handler(parsed_params)
content = ResultEncoder().encode(result) content = ResultEncoder().encode(result)
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'text/plain') self.send_header('Content-Type', 'text/plain')
self.send_header('Content-Length', str(len(content))) self.send_header('Content-Length', str(len(content)))
self.end_headers() self.end_headers()
self.wfile.write(content) self.wfile.write(content)
   
def handle_json_GET_routes(self, params): def handle_json_GET_routes(self, params):
"""Return a list of all routes.""" """Return a list of all routes."""
schedule = self.server.schedule schedule = self.server.schedule
result = [] result = []
for r in schedule.GetRouteList(): for r in schedule.GetRouteList():
servicep = None servicep = None
for t in schedule.GetTripList(): for t in schedule.GetTripList():
if t.route_id == r.route_id: if t.route_id == r.route_id:
servicep = t.service_period servicep = t.service_period
break break
result.append( (r.route_id, r.route_short_name, r.route_long_name, servicep.service_id) ) result.append( (r.route_id, r.route_short_name, r.route_long_name, servicep.service_id) )
result.sort(key = lambda x: x[1:3]) result.sort(key = lambda x: x[1:3])
return result return result
   
def handle_json_GET_routerow(self, params): def handle_json_GET_routerow(self, params):
schedule = self.server.schedule schedule = self.server.schedule
route = schedule.GetRoute(params.get('route', None)) route = schedule.GetRoute(params.get('route', None))
return [transitfeed.Route._FIELD_NAMES, route.GetFieldValuesTuple()] return [transitfeed.Route._FIELD_NAMES, route.GetFieldValuesTuple()]
def handle_json_GET_routetrips(self, params): def handle_json_GET_routetrips(self, params):
""" Get a trip for a route_id (preferablly the next one) """ """ Get a trip for a route_id (preferablly the next one) """
schedule = self.server.schedule schedule = self.server.schedule
query = params.get('route_id', None).lower() query = params.get('route_id', None).lower()
result = [] result = []
for t in schedule.GetTripList(): for t in schedule.GetTripList():
if t.route_id == query: if t.route_id == query:
result.append ( (t.GetStartTime(), t.trip_id) ) result.append ( (t.GetStartTime(), t.trip_id) )
return sorted(result, key=lambda trip: trip[0]) return sorted(result, key=lambda trip: trip[0])
def handle_json_GET_triprows(self, params): def handle_json_GET_triprows(self, params):
"""Return a list of rows from the feed file that are related to this """Return a list of rows from the feed file that are related to this
trip.""" trip."""
schedule = self.server.schedule schedule = self.server.schedule
try: try:
trip = schedule.GetTrip(params.get('trip', None)) trip = schedule.GetTrip(params.get('trip', None))
except KeyError: except KeyError:
# if a non-existent trip is searched for, the return nothing # if a non-existent trip is searched for, the return nothing
return return
route = schedule.GetRoute(trip.route_id) route = schedule.GetRoute(trip.route_id)
trip_row = dict(trip.iteritems()) trip_row = dict(trip.iteritems())
route_row = dict(route.iteritems()) route_row = dict(route.iteritems())
return [['trips.txt', trip_row], ['routes.txt', route_row]] return [['trips.txt', trip_row], ['routes.txt', route_row]]
   
def handle_json_GET_tripstoptimes(self, params): def handle_json_GET_tripstoptimes(self, params):
schedule = self.server.schedule schedule = self.server.schedule
try: try:
trip = schedule.GetTrip(params.get('trip')) trip = schedule.GetTrip(params.get('trip'))
except KeyError: except KeyError:
# if a non-existent trip is searched for, the return nothing # if a non-existent trip is searched for, the return nothing
return return
time_stops = trip.GetTimeStops() time_stops = trip.GetTimeInterpolatedStops()
stops = [] stops = []
times = [] times = []
for arr,dep,stop in time_stops: for arr,ts,is_timingpoint in time_stops:
stops.append(StopToTuple(stop)) stops.append(StopToTuple(ts.stop))
times.append(arr) times.append(arr)
return [stops, times] return [stops, times]
   
def handle_json_GET_tripshape(self, params): def handle_json_GET_tripshape(self, params):
schedule = self.server.schedule schedule = self.server.schedule
try: try:
trip = schedule.GetTrip(params.get('trip')) trip = schedule.GetTrip(params.get('trip'))
except KeyError: except KeyError:
# if a non-existent trip is searched for, the return nothing # if a non-existent trip is searched for, the return nothing
return return
points = [] points = []
if trip.shape_id: if trip.shape_id:
shape = schedule.GetShape(trip.shape_id) shape = schedule.GetShape(trip.shape_id)
for (lat, lon, dist) in shape.points: for (lat, lon, dist) in shape.points:
points.append((lat, lon)) points.append((lat, lon))
else: else:
time_stops = trip.GetTimeStops() time_stops = trip.GetTimeStops()
for arr,dep,stop in time_stops: for arr,dep,stop in time_stops:
points.append((stop.stop_lat, stop.stop_lon)) points.append((stop.stop_lat, stop.stop_lon))
return points return points
   
def handle_json_GET_neareststops(self, params): def handle_json_GET_neareststops(self, params):
"""Return a list of the nearest 'limit' stops to 'lat', 'lon'""" """Return a list of the nearest 'limit' stops to 'lat', 'lon'"""
schedule = self.server.schedule schedule = self.server.schedule
lat = float(params.get('lat')) lat = float(params.get('lat'))
lon = float(params.get('lon')) lon = float(params.get('lon'))
limit = int(params.get('limit')) limit = int(params.get('limit'))
stops = schedule.GetNearestStops(lat=lat, lon=lon, n=limit) stops = schedule.GetNearestStops(lat=lat, lon=lon, n=limit)
return [StopToTuple(s) for s in stops] return [StopToTuple(s) for s in stops]
   
def handle_json_GET_boundboxstops(self, params): def handle_json_GET_boundboxstops(self, params):
"""Return a list of up to 'limit' stops within bounding box with 'n','e' """Return a list of up to 'limit' stops within bounding box with 'n','e'
and 's','w' in the NE and SW corners. Does not handle boxes crossing and 's','w' in the NE and SW corners. Does not handle boxes crossing
longitude line 180.""" longitude line 180."""
schedule = self.server.schedule schedule = self.server.schedule
n = float(params.get('n')) n = float(params.get('n'))
e = float(params.get('e')) e = float(params.get('e'))
s = float(params.get('s')) s = float(params.get('s'))
w = float(params.get('w')) w = float(params.get('w'))
limit = int(params.get('limit')) limit = int(params.get('limit'))
stops = schedule.GetStopsInBoundingBox(north=n, east=e, south=s, west=w, n=limit) stops = schedule.GetStopsInBoundingBox(north=n, east=e, south=s, west=w, n=limit)
return [StopToTuple(s) for s in stops] return [StopToTuple(s) for s in stops]
   
def handle_json_GET_stops(s