Initial commit

This commit is contained in:
IamTheFij 2014-11-22 17:05:03 -08:00
commit 0f66eee72b
20 changed files with 733 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dev.db
.bundle

16
Gemfile Normal file
View File

@ -0,0 +1,16 @@
source 'https://rubygems.org'
ruby '1.9.3'
gem 'sinatra'
gem 'activerecord'
gem 'sinatra-activerecord'
gem 'httparty'
gem 'geocoder'
gem 'algorithms'
gem 'levenshtein-ffi', :require => 'levenshtein'
group :development, :test do
gem 'sqlite3'
end
group :production, :staging do
gem 'pg'
end

65
Gemfile.lock Normal file
View File

@ -0,0 +1,65 @@
GEM
remote: https://rubygems.org/
specs:
activemodel (4.0.2)
activesupport (= 4.0.2)
builder (~> 3.1.0)
activerecord (4.0.2)
activemodel (= 4.0.2)
activerecord-deprecated_finders (~> 1.0.2)
activesupport (= 4.0.2)
arel (~> 4.0.0)
activerecord-deprecated_finders (1.0.3)
activesupport (4.0.2)
i18n (~> 0.6, >= 0.6.4)
minitest (~> 4.2)
multi_json (~> 1.3)
thread_safe (~> 0.1)
tzinfo (~> 0.3.37)
algorithms (0.6.1)
arel (4.0.1)
atomic (1.1.14)
builder (3.1.4)
ffi (1.1.5)
geocoder (1.1.9)
httparty (0.12.0)
json (~> 1.8)
multi_xml (>= 0.5.2)
i18n (0.6.9)
json (1.8.1)
levenshtein-ffi (1.0.3)
ffi
ffi (~> 1.1.5)
minitest (4.7.5)
multi_json (1.8.4)
multi_xml (0.5.5)
pg (0.17.1)
rack (1.5.2)
rack-protection (1.5.1)
rack
sinatra (1.4.4)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
sinatra-activerecord (1.2.3)
activerecord (>= 3.0)
sinatra (~> 1.0)
sqlite3 (1.3.8)
thread_safe (0.1.3)
atomic
tilt (1.4.1)
tzinfo (0.3.38)
PLATFORMS
ruby
DEPENDENCIES
activerecord
algorithms
geocoder
httparty
levenshtein-ffi
pg
sinatra
sinatra-activerecord
sqlite3

18
README.md Normal file
View File

@ -0,0 +1,18 @@
Clear Transit Server
====================
What is this?
-------------
This is a server component of Clear Transit. An Android and Google Glass application for retrieving transit times from the NextBus Api
The Android client for this Heroku app is located at [Clear Transit](https://github.com/IamTheFij/ClearTransit")
I'm really not much of a Ruby dev, so this may be a little hacky. It's designed to run on Heroku using a Postgres DB.
The initial database is built by running: `ruby ./build_agency_bounds.rb` or, if it fails midway, running `ruby ./continue_agency_bounds.rb`.
Heroku button
-------------
This buttons should install the server on Heroku. Should... no promises.
[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/IamTheFij/ClearTransitServer)

2
Rakefile Normal file
View File

@ -0,0 +1,2 @@
require './routes.rb'
require 'sinatra/activerecord/rake'

18
app.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "Clear Transit",
"description": "Clear Transit web server",
"keywords": [
"transit",
"Sinatra",
"Ruby"
],
"website": "https://github.com/IamTheFij/ClearTransit-Web",
"repository": "https://github.com/IamTheFij/ClearTransit-Web",
"success_url": "/working",
"scripts": {
"postdeploy": "ruby ./build_agency_bounds.rb"
},
"addons": [
"heroku-postgresql:hobby-dev"
]
}

48
build_agency_bounds.rb Normal file
View File

@ -0,0 +1,48 @@
require './environments.rb'
require './models/active.rb'
require './lib/nextbus.rb'
require './lib/utils.rb'
agencies = {}
AgencyBound.all.each do |bound|
bound.lat_min = nil
bound.lat_max = nil
bound.lon_min = nil
bound.lon_max = nil
agencies[bound.agency] = bound
end
NextBus.agency_list.parsed_response['agency'].each do |agency|
unless agencies.has_key?(agency['tag'])
agencies[agency['tag']] = AgencyBound.new(
agency: agency['tag'],
lat_min: nil,
lat_max: nil,
lon_min: nil,
lon_max: nil
)
end
end
agencies.each_value do |bound|
routes = NextBus.routes(bound.agency).parsed_response['route']
print "#{routes}\n"
unless routes.nil?
unless routes.kind_of?(Array)
routes = [ routes ]
end
routes.each do |route|
route_config = NextBus.route_config(bound.agency, route['tag']).parsed_response
#print "Route Config:\n#{route_config}\n"
route_config = route_config['route']
print "Agency: #{bound.agency} Route: #{route['tag']} Bound Min: #{bound.lat_min}, Config Min #{route_config['latMin']}\n"
bound.lat_min = min_val(bound.lat_min, route_config['latMin'].to_f)
bound.lat_max = max_val(bound.lat_max, route_config['latMax'].to_f)
bound.lon_min = min_val(bound.lon_min, route_config['lonMin'].to_f)
bound.lon_max = max_val(bound.lon_max, route_config['lonMax'].to_f)
end
bound.save
end
end

126
cleartransit.rb Normal file
View File

@ -0,0 +1,126 @@
require 'sinatra'
#require 'sinatra/activerecord'
require 'json'
require 'geocoder'
require 'algorithms'
require 'levenshtein'
require './environments.rb'
require './models/active.rb'
require './lib/nextbus.rb'
require './lib/utils.rb'
# Uses bounds of the user and returns any agencies they are in the bounds of
def get_contained_agencies(lat, lon)
agencies = []
AgencyBound.all.each do |bound|
if lat.between?(bound.lat_min, bound.lat_max) and lon.between?(bound.lon_min, bound.lon_max)
agencies.push(bound.agency)
end
end
return agencies
end
# Takes user input and agency and tries to find the most likely intended route
def find_best_routes(agency, user_route)
# Remove Line from end of user_route
user_route = user_route.downcase
user_route_words = user_route.split
if user_route_words.last == 'line'
user_route_words.pop
end
user_route = user_route_words.join(' ')
# TODO: Remove before Prod
routes = NextBus.routes(agency).parsed_response
#routes = {
# 'route' => [
# {
# 'title' => 'F - Market Warves',
# 'tag' => 'F'
# },
# {
# 'title' => 'P - ',
# 'tag' => 'F'
# }
# ]
#}
probable_routes = Containers::MinHeap.new
routes['route'].each do |route|
dist = min_val(Levenshtein.distance(user_route, route['title'].downcase), Levenshtein.distance(user_route, route['tag'].downcase))
route_match = RouteMatch.new(
user_route: user_route,
match_route: route['tag'],
match_title: route['title'],
agency: agency,
# TODO: Depending on App implementation, default to false and set true or oposite
accepted: false,
distance: dist
)
probable_routes.push(dist, route_match)
end
routes = []
while not probable_routes.empty? and routes.length < 10
routes.push(probable_routes.pop)
end
return routes
end
# Takes an agency, route, and lat-lon and returns nearest predictions
def get_nearest_predictions(agency, route, lat, lon)
current_location = [lat, lon]
print current_location
route_config = NextBus.route_config(agency, route).parsed_response
min_stops = Containers::MinHeap.new
route_config['route']['stop'].each do |stop|
dist = Geocoder::Calculations.distance_between(current_location, [stop['lat'], stop['lon']])
min_stops.push(dist, stop)
end
#stop_directions = build_direction_map(route_config)
stops = [], route_stops = []
while not min_stops.empty? and stops.length < 5
stop = min_stops.pop
# Build array out of the 5 closest stops
stops.push(stop)
# Push route stops for fetching predictions
route_stops.push({:route => route, :stop => stop['tag']})
end
min_stops.clear
prediction_multiple = NextBus.prediction_multiple(agency, route_stops).parsed_response
# TODO: Possibly adjust sort order here if these appear to be wrong
return prediction_multiple
end
def build_direction_map(route_config)
# TODO: DEPRECIATE
stop_directions = {}
route_config['route']['direction'].each do |direction|
direction['stop'].each do |stop|
unless stop_directions[stop['tag']]
stop_directions[stop['tag']] = []
end
stop_directions[stop['tag']].push(direction['name'])
end
end
return stop_directions
end

2
config.ru Normal file
View File

@ -0,0 +1,2 @@
require './routes.rb'
run Sinatra::Application

54
continue_agency_bounds.rb Normal file
View File

@ -0,0 +1,54 @@
require './environments.rb'
require './models/active.rb'
require './lib/nextbus.rb'
require './lib/utils.rb'
agencies = {}
AgencyBound.all.each do |bound|
if bound.lat_max != nil
agencies[bound.agency] = bound
end
end
NextBus.agency_list.parsed_response['agency'].each do |agency|
if agencies.has_key?(agency['tag'])
agencies.delete(agency['tag'])
else
agencies[agency['tag']] = AgencyBound.new(
agency: agency['tag'],
lat_min: nil,
lat_max: nil,
lon_min: nil,
lon_max: nil
)
end
end
print "Agencies to get? #{agencies}"
if agencies.empty?
print "No new agencies"
end
agencies.each_value do |bound|
routes = NextBus.routes(bound.agency).parsed_response['route']
print "#{routes}\n"
unless routes.nil?
unless routes.kind_of?(Array)
routes = [ routes ]
end
routes.each do |route|
route_config = NextBus.route_config(bound.agency, route['tag']).parsed_response
#print "Route Config:\n#{route_config}\n"
route_config = route_config['route']
print "Agency: #{bound.agency} Route: #{route['tag']} Bound Min: #{bound.lat_min}, Config Min #{route_config['latMin']}\n"
bound.lat_min = min_val(bound.lat_min, route_config['latMin'].to_f)
bound.lat_max = max_val(bound.lat_max, route_config['latMax'].to_f)
bound.lon_min = min_val(bound.lon_min, route_config['lonMin'].to_f)
bound.lon_max = max_val(bound.lon_max, route_config['lonMax'].to_f)
end
bound.save
end
end

View File

@ -0,0 +1,27 @@
class InitialDb < ActiveRecord::Migration
def up
create_table :route_matches do |t|
t.string :user_route
t.string :match_route
t.string :match_title
t.integer :distance
t.string :agency
t.boolean :accepted
t.timestamps
end
create_table :agency_bounds do |t|
t.string :agency
t.float :lat_max
t.float :lat_min
t.float :lon_max
t.float :lon_min
t.timestamps
end
end
def down
drop_table :route_matches
drop_table :agency_bounds
end
end

36
db/schema.rb Normal file
View File

@ -0,0 +1,36 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140118012229) do
create_table "agency_bounds", force: true do |t|
t.string "agency"
t.float "lat_max"
t.float "lat_min"
t.float "lon_max"
t.float "lon_min"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "route_matches", force: true do |t|
t.string "user_route"
t.string "match_route"
t.string "match_title"
t.integer "distance"
t.string "agency"
t.boolean "accepted"
t.datetime "created_at"
t.datetime "updated_at"
end
end

29
environments.rb Normal file
View File

@ -0,0 +1,29 @@
require 'cgi'
require 'uri'
require 'sinatra'
require 'sinatra/activerecord'
configure :development do
set :database, 'sqlite:///dev.db'
set :show_exceptions, true
end
configure :staging, :production do
begin
db = URI.parse(ENV["DATABASE_URL"])
rescue URI::InvalidURIError
raise "Invalid DATABASE_URL"
end
ActiveRecord::Base.establish_connection(
:adapter => db.scheme == 'postgres' ? 'postgresql' : db.scheme,
:encoding => 'unicode',
:pool => 5,
:database => db.path[1..-1],
:username => db.user,
:password => db.password,
:host => db.host,
:port => db.port
)
end

41
lib/nextbus.rb Normal file
View File

@ -0,0 +1,41 @@
require 'httparty'
class NextBus
include HTTParty
def self.get_command(command, query={})
# Set the command
query[:command] = command
get('http://webservices.nextbus.com/service/publicJSONFeed', :query => query)
end
def self.agency_list
get_command('agencyList')
end
def self.routes(agency)
get_command('routeList', { :a => agency })
end
def self.route_config(agency, route)
get_command('routeConfig', { :a => agency, :r => route })
end
def self.prediction(agency, route, stop, short_titles=true)
get_command('predictions', { :a => agency, :r => route, :s => stop, :useShortTitles => short_titles })
end
def self.prediction_multiple(agency, route_stops=[])
endpoint = 'http://webservices.nextbus.com/service/publicJSONFeed'
endpoint += '?command=predictionsForMultiStops'
endpoint += '&a=' + agency
route_stops.each do |route_stop|
endpoint += '&stops=' + route_stop[:route] + '%7C' + route_stop[:stop]
end
print endpoint
get(endpoint)
end
end

35
lib/utils.rb Normal file
View File

@ -0,0 +1,35 @@
def min_val(v1, v2)
if v2 == nil
return v1
elsif v1 == nil
return v2
elsif v1 < v2
return v1
else
return v2
end
end
def max_val(v1, v2)
if v2 == nil
return v1
elsif v1 == nil
return v2
elsif v1 > v2
return v1
else
return v2
end
end
def is_number?(object)
true if Float(object) rescue false
end
def to_f_nil(s)
begin
return Float(s)
rescue
return nil
end
end

5
models/active.rb Normal file
View File

@ -0,0 +1,5 @@
# Require individual models here
require 'active_record'
require './models/agency_bounds.rb'
require './models/route_matches.rb'

8
models/agency_bounds.rb Normal file
View File

@ -0,0 +1,8 @@
# Table for storing boundries of given agencies to auto-detect
class AgencyBound < ActiveRecord::Base
validates :agency, :lat_max, :lat_min, :lon_max, :lon_min, presence: true
# validates :lat_max, presence: true
# validates :lat_min, presence: true
# validates :lon_max, presence: true
# validates :lon_min, presence: true
end

9
models/route_matches.rb Normal file
View File

@ -0,0 +1,9 @@
# Table for storing the user input and levenshtein match
# Will be used to assess accuracy and user acceptance
class RouteMatch < ActiveRecord::Base
validates :user_route, :match_route, :distance, presence: true
# validates :match_route, presence: true
# validates :distance, presence: true
# TODO: Default value for accepted?
end

168
routes.rb Normal file
View File

@ -0,0 +1,168 @@
require 'sinatra'
require 'sinatra/activerecord'
require 'json'
require './cleartransit.rb'
require './lib/nextbus.rb'
get '/' do
# TODO: Provide link to download the app
'Hello world! Go check out the repo: <a href="https://github.com/IamTheFij/CanHazMuni-Web">CanHazMuni-Web</a>'
end
get '/working' do
'Hello world! Go check out the repo: <a href="https://github.com/IamTheFij/CanHazMuni-Web">CanHazMuni-Web</a>'
end
# List all agencies
get '/agency' do
status 200
body(NextBus.agency_list.to_json)
end
# List all routes in an agency
get '/agency/:agency/route' do |agency|
status 200
body(NextBus.routes(agency).to_json)
end
# List all stops in a route
get '/agency/:agency/route/:route/stop' do |agency, route|
status 200
body(NextBus.route_config(agency, route).to_json)
end
# Get predictions for a given stop
get '/agency/:agency/route/:route/stop/:stop' do |agency, route, stop|
status 200
body(NextBus.prediction(agency, route, stop).to_json)
end
# Get predictions for nearest stop on given agency and route
get '/agency/:agency/route/:route/nearest' do |agency, route|
# will match /agency/sf-muni/route/F/nearest?lat=37.804016399999995&lon=-122.40376609999998
lat = to_f_nil(params[:lat])
lon = to_f_nil(params[:lon])
if params[:lat].nil? or lon.nil? or route.nil?
status 404
else
status 200
body(get_nearest_predictions(agency, route, lat, lon).to_json)
end
end
# Primary feature
# Usine Lat, Lon, determine nearest agency, and stops and provide predictions
=begin rdoc
get '/icanhaz' do
# will match /icanhaz?lat=37.804016399999995&lon=-122.40376609999998&route=F%20line
if params[:lat].nil? or params[:lon].nil? or params[:route].nil?
status 404
else
status 200
# TODO: Implement
body({:thing1 => 'value'}.to_json)
end
end
=end
# Usine Lat, Lon and given agency, and user route and provide predictions
get '/icanhaz/:agency' do |agency|
# will match /icanhaz/sf-muni?lat=37.804016399999995&lon=-122.40376609999998&route=F%20line
lat = to_f_nil(params[:lat])
lon = to_f_nil(params[:lon])
if agency.nil? or lat.nil? or lon.nil? or params[:route].nil?
body("Error: Post convert agency: #{agency} lat: #{lat} lon: #{lon} route: #{params[:route]}")
status 404
else
status 200
# Use only the best route for now
route = find_best_routes(agency, params[:route])[0]
body(get_nearest_predictions(agency, route[:tag], params[:lat], params[:lon]).to_json)
end
end
get '/icanhaz/agency/route' do
# will match /icanhaz/agency/route?lat=37.804016399999995&lon=-122.40376609999998&route=F%20line
lat = to_f_nil(params[:lat])
lon = to_f_nil(params[:lon])
if lat.nil? or lon.nil? or params[:route].nil?
body("Error: Post convert lat: #{lat} lon: #{lon} route: #{params[:route]}")
status 404
else
agencies = get_contained_agencies(lat, lon)
if agencies.empty?
# TODO: Find better status code for something like this
status 404
body("No service area found")
else
status 200
agency = agencies[0]
user_route = params[:route];
# Get the most likely route in the agency
# TODO: Match all the results returned to the RouteMatch class and insert all.
# This will be super useful to provide a scroll list for the user to pick
# alternate matches. The client can then mark which was actually matched.
# When doing this, will probably want a unique "session" Id as well
route_match = find_best_routes(agency, user_route)[0]
route_match.save
body(route_match.to_json)
end
end
end
# Usine given agency, and user route, guess the route
get '/icanhaz/:agency/route' do |agency|
# will match /icanhaz/sf-muni/route?route=F%20line
if agency.nil? or params[:route].nil?
status 404
else
status 200
user_route = params[:route];
# Get the most likely route in the agency
# TODO: Match all the results returned to the RouteMatch class and insert all.
# This will be super useful to provide a scroll list for the user to pick
# alternate matches. The client can then mark which was actually matched.
# When doing this, will probably want a unique "session" Id as well
route_match = find_best_routes(agency, user_route)[0]
route_match.save
body(route_match.to_json)
end
end
# Usine given agency, and actual route, get the predictions
get '/icanhaz/:agency/route/:route' do |agency, route|
# will match /icanhaz/sf-muni/route/F?lat=37.804016399999995&lon=-122.40376609999998
# Optionally takes &match_id=123
lat = to_f_nil(params[:lat])
lon = to_f_nil(params[:lon])
if agency.nil? or route.nil? or lat.nil? or lon.nil?
body("Error: Post convert lat: #{lat} lon: #{lon} route: #{params[:route]}")
status 404
else
status 200
# Get Id of databse tracked route match and indicate that it was accepted for predictions
if not params[:match_id].nil?
RouteMatch.update(params[:match_id], :accepted => true)
end
body(get_nearest_predictions(agency, route, lat, lon).to_json)
end
end
get '/route_matches' do
status 200
body(RouteMatch.all.to_json)
end

24
tags Normal file
View File

@ -0,0 +1,24 @@
!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/
!_TAG_PROGRAM_NAME Exuberant Ctags //
!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/
!_TAG_PROGRAM_VERSION 5.8 //
AgencyBound models/agency_bounds.rb /^class AgencyBound < ActiveRecord::Base$/;" c
InitialDb db/migrate/20140118012229_initial_db.rb /^class InitialDb < ActiveRecord::Migration$/;" c
NextBus lib/nextbus.rb /^class NextBus$/;" c
RouteMatch models/route_matches.rb /^class RouteMatch < ActiveRecord::Base$/;" c
agency_list lib/nextbus.rb /^ def self.agency_list$/;" F class:NextBus
build_direction_map canhazmuni.rb /^def build_direction_map(route_config)$/;" f
down db/migrate/20140118012229_initial_db.rb /^ def down$/;" f class:InitialDb
find_best_routes canhazmuni.rb /^def find_best_routes(agency, user_route)$/;" f
get_command lib/nextbus.rb /^ def self.get_command(command, query={})$/;" F class:NextBus
get_contained_agencies canhazmuni.rb /^def get_contained_agencies(lat, lon)$/;" f
get_nearest_predictions canhazmuni.rb /^def get_nearest_predictions(agency, route, lat, lon)$/;" f
max_val lib/utils.rb /^def max_val(v1, v2)$/;" f
min_val lib/utils.rb /^def min_val(v1, v2)$/;" f
prediction lib/nextbus.rb /^ def self.prediction(agency, route, stop, short_titles=true)$/;" F class:NextBus
prediction_multiple lib/nextbus.rb /^ def self.prediction_multiple(agency, route_stops=[])$/;" F class:NextBus
route_config lib/nextbus.rb /^ def self.route_config(agency, route)$/;" F class:NextBus
routes lib/nextbus.rb /^ def self.routes(agency)$/;" F class:NextBus
up db/migrate/20140118012229_initial_db.rb /^ def up$/;" f class:InitialDb