Course:CPSC312-2017-BusStopFinder

From UBC Wiki

Title

Authors: Andy Lin, Gareth Ellis, Noa Avigad

What is the problem?

Investigate the feasibility of using logic programming to query and present to the user the location of the nearest bus stop to them (or to a predefined location) and present the bus schedule for that bus stop.

What is the something extra?

Working with TransLink's REST API, fetching, parsing and displaying the relevant data.

What did we learn from doing this?

While we expected working with REST API's to be difficult in a logic programming language such as Prolog, we were pleasantly surprised to find the learning curve for working with such API's quite small. The principle difficulties were general lack of documentation and examples for working with HTTP requests, as well as parsing data from the received JSON. Overall, we found logic programming to be entirely suitable to the task, as evedinced by the fact that three people with minimal logic programming experience were, in less then two weeks, able to create a system for querying bus stops throughout Vancouver in a variety of ways.

Note: For the purposes of posting publicly on the Wiki, we have not included the TransLink API key that we used to make requests to TransLink's REST API. Thus, if any of the TA's wish to run the code, we can show you in the demo or you can email us to get the API key.

Code

bus_stops_around.pl

:- [util].
:- [geolocation].
:- [translinkAPI].

% The default radius to look for bus stops in
defaultRadius(200).

% stopNear(POI, BusStopName) is true when BusStopName is within 200 meters radius from POI
stopNear(POI, BusStopName) :-
    getLatLon(POI, Lat, Lon),
    defaultRadius(R),
    busStopsJSON(Lat, Lon, R),
    queryBusStop(_, 'Name', BusStopName).


% If we just have a radius, need to look up users lat and lon
getNearbyBusStops(Radius) :-
    number(Radius),
    getGeoLocation(Lat, Lon),
    getNearbyBusStops(Lat, Lon, Radius).

% If we just have a location, use the default radius
getNearbyBusStops(Location) :-
    defaultRadius(Radius),
    getNearbyBusStops(Location, Radius).

% If we have a lat, lon, and Radius, can just call Translink API directly
getNearbyBusStops(Lat, Lon, Radius) :-
    busStopsJSON(Lat, Lon, Radius).

% If we just have a location and radius, need to get the locations lat and lon first
getNearbyBusStops(Location, Radius) :-
    getLatLon(Location, Lat, Lon),
    getNearbyBusStops(Lat, Lon, Radius).

% If we just have a lat and lon, use the default radius
getNearbyBusStops(Lat, Lon) :-
    defaultRadius(Radius),
    getNearbyBusStops(Lat, Lon, Radius).

% If we have no information, just use the users current lat and lon,
% and the default radius
getNearbyBusStops() :-
    defaultRadius(Radius),
    getGeoLocation(Lat, Lon),
    getNearbyBusStops(Lat, Lon, Radius).
 

geolocation.pl

:- [util].
:- [geolocation].
:- [translinkAPI].

% The default radius to look for bus stops in
defaultRadius(200).

% stopNear(POI, BusStopName) is true when BusStopName is within 200 meters radius from POI
stopNear(POI, BusStopName) :-
    getLatLon(POI, Lat, Lon),
    defaultRadius(R),
    busStopsJSON(Lat, Lon, R),
    queryBusStop(_, 'Name', BusStopName).


% If we just have a radius, need to look up users lat and lon
getNearbyBusStops(Radius) :-
    number(Radius),
    getGeoLocation(Lat, Lon),
    getNearbyBusStops(Lat, Lon, Radius).

% If we just have a location, use the default radius
getNearbyBusStops(Location) :-
    defaultRadius(Radius),
    getNearbyBusStops(Location, Radius).

% If we have a lat, lon, and Radius, can just call Translink API directly
getNearbyBusStops(Lat, Lon, Radius) :-
    busStopsJSON(Lat, Lon, Radius).

% If we just have a location and radius, need to get the locations lat and lon first
getNearbyBusStops(Location, Radius) :-
    getLatLon(Location, Lat, Lon),
    getNearbyBusStops(Lat, Lon, Radius).

% If we just have a lat and lon, use the default radius
getNearbyBusStops(Lat, Lon) :-
    defaultRadius(Radius),
    getNearbyBusStops(Lat, Lon, Radius).

% If we have no information, just use the users current lat and lon,
% and the default radius
getNearbyBusStops() :-
    defaultRadius(Radius),
    getGeoLocation(Lat, Lon),
    getNearbyBusStops(Lat, Lon, Radius).
 

translinkAPI.pl

% Used to load data (JSON/XML/HTML) from a given URL
:- use_module(library(http/http_client)).
:- use_module(library(http/json)).
:- [util].

% The API key for accessing Translink's API
translinkApiKey("").

% Predicates used for storing information
:- dynamic
    triple/3,
    user_info/3,
    stopNumber/1.

% `Data` is the JSON response from the Translink API representing all the stops
% in a given `Radius` of a given `Lat` and `Lon`
% Ex: busStopsJSON(49.187706, -122.850060, 200).
busStopsJSON(Lat, Lon, Radius) :-
    validate_input(Lat, Lon, Radius),
    clear_KB(),
    translinkApiKey(Key),
    atomic_list_concat(['http://api.translink.ca/rttiapi/v1/stops?apikey=', 
                Key, '&lat=', Lat, '&long=', Lon, '&radius=', Radius], URL),
    http_get(URL, RawJSON, [request_header('Accept'='application/json')]),
    atom_json_term(RawJSON, JSON, []),
    parseStopList(JSON).

% input validation:
% Lat, Lon and Radius are numbers
% Radius is greater than 0 and smaller than 2000 (Translink API max radius)
validate_input(Lat, Lon, Rad) :-
    number(Lat),
    number(Lon),
    number(Rad),
    Rad < 2000, Rad > 0.
validate_input(_, _, Rad) :- 
    ( Rad > 2000
    -> print('Given radius is invalid, please choose a radius between 0 and 2000')
    ; _ = 1 
    ).
validate_input(_, _, Rad) :- 
    ( Rad < 0 
    -> print('Given radius is invalid, please choose a radius between 0 and 2000')
    ; _ = 1 
    ).

% Clearing the KB
clear_KB() :-
    retractall(triple(_, _, _)).

% parseStopList(L) is true if all of the stops formated in JSON are successfully parsed into its attributes and inserted into the KB.
% Each item in the list of stops that is the first argument of parseStopList(L) has the format 
% json([StopNo=...,Name=...,BayNo=...,City=...,OnStreet=...,AtStreet=...,Latitude=...,Longitude= ...,WheelchairAccess=...,Distance=...,Routes=...])
% First the stop number is extracted from the JSON attributes then the rest of the JSON attributes are parsed and inserted into the KB.
% Then parseStopList() recursively parses through the rest of the list of stops
parseStopList([H1 | T1]) :-
    arg(1, H1, [JsonAttribute1 | JsonAttributeList]),
    parseStopNumber(JsonAttribute1, StopNumber),
    parseJSON(JsonAttributeList, StopNumber),
    parseStopList(T1).

% Base case stating that parsing the stop list of an empty list is true.
parseStopList([]).

% parseStopNumber(JsonAttribute1, StopNumber) is true if the stop number is successfully extracted from the provided JsonAttribute1 and the triple containing the stop number is inserted into the KB
% The JsonAttribute1 argument is of the format StopNo=...
% First the atom before and after the = sign is parsed and then the quotes from the atoms are removed and the stop number is inserted into the KB as a triple with the property StopNo. 
parseStopNumber(H, StopNumber) :-
    term_to_atom(H, Atom),
    beforeAndAfter(Atom, Before, StopNumber, '='),
    atomic_list_concat([stop, StopNumber], '_', Individual),
    removeQuotes(triple(Individual, Before, StopNumber), TripleNoQuotes),
    assert(TripleNoQuotes).

% parseJSON(L, StopNumber) is true if the list of JSON attributes in L are correctly parsed into their properties and values and then inserted into the KB as a triple.
parseJSON([H | T], StopNumber) :- 
    term_to_atom(H, Atom),
    beforeAndAfter(Atom, Before, After, '='),
    atomic_list_concat([stop, StopNumber], '_', Individual),
    removeQuotes(triple(Individual, Before, After), TripleNoQuotes),
    assert(TripleNoQuotes),
    parseJSON(T, StopNumber).

% Base case of parseJSON() which states that an empty JSON attribute list is always parsed correctly.
parseJSON([], _).

% queryBusStop(Individual, Property, Value) is true only if the predicate triple/3 exists in the KB and the triple with the three arguments is true in the KB.
queryBusStop(Individual, Property, Value) :- current_predicate(triple/3), triple(Individual, Property, Value).

% queryUserInformation(Individual, Property, Value) is true only if the predicate user_info/3 exists in the KB, the user_info predicate with the three arguments is true in the KB and the current property is a valid geolocation attribute
queryUserInformation(Individual, Property, Value) :- current_predicate(user_info/3), user_info(Individual, Property, Value), validGeolocationAttribute(Property).
 

util.pl

% Gets everything `Before` and `After` `char` in a string
beforeAndAfterChar([NotChar|T],[NotChar|Before],After,Char) :- 
    dif(Char, NotChar),
    beforeAndAfterChar(T,Before,After,Char).
beforeAndAfterChar([Char|After],[],After,Char). 
beforeAndAfterChar(String,Before,After,Char) :-
    \+ is_list(String),
    atom_chars(String, StringChars),
    beforeAndAfterChar(StringChars,Before,After,Char).

beforeAndAfter(Atom, Before, After, Char) :-
    atom_string(Atom, String),
    beforeAndAfterChar(String, BeforeArray, AfterArray, Char),
    atom_chars(Before, BeforeArray),
    atom_chars(After, AfterArray).

% `tripleNoQuotes` is `triple(I, P, V)` but with single quotes removed from
% I, P, and V
removeQuotes(triple(I,P,V), triple(INoQuotes, PNoQuotes, VNoQuotes)) :-
    removeChar(I, INoQuotes, '\''),
    removeChar(P, PNoQuotes, '\''),
    removeChar(V, VNoQuotes, '\'').

% `StrCharRemoved` is `Str`, but with a single `Char` removed
removeChar(Str, StrCharRemoved, Char) :-
    atom_chars(Str, StrList),
    removeCharFromList(StrList, StrListCharRemoved, Char),
   atom_chars(StrCharRemoved, StrListCharRemoved).
removeCharFromList([], [], _).
removeCharFromList([Char|Str], StrCharRemoved, Char) :-
    removeCharFromList(Str, StrCharRemoved, Char).
removeCharFromList([A|Str], [A|StrCharRemoved], Char) :-
    dif(A, Char),
    removeCharFromList(Str,StrCharRemoved,Char).

% getLatLon(POI, Lat, Lon) is true if Lat and Lon correspond to the given point of interest
getLatLon(cpsc_building, 49.260986, -123.248064).
getLatLon(city_hall, 49.263248, -123.114934).
getLatLon(nest, 49.266502, -123.249666).
getLatLon(bcit, 49.249149, -123.001068).
getLatLon(sfu, 49.279171, -122.919808).
getLatLon(west_end, 49.291525, -123.135465).