Source: org/terraswarm/accessor/accessors/web/services/SemanticYelpSearch.js

/** Perform a Yelp search for local businesses using the Yelp API
 *  https://www.yelp.com/developers/documentation/v3/business_search
 * 
 *  The full Yelp API allows many parameters for searching for business.
 *  This accessor demonstrates a simple query for a particular type of
 *  business near specific geographic coordinates.
 *
 *  This accessor extends the YelpSearch accessor to produce a schema.org compatible
 *  Turtle ontology. Since schema.org is a linked data ontology, additional information
 *  about the ontology can be found by checking out the URIs in the output.
 *
 *  @accessor services/SemanticYelpSearch
 *  @author Matt Weber
 *  @version $$Id: SemanticYelpSearch.js 1725 2017-05-19 22:59:11Z cxh $$
 *  @input {string} searchTerm The variety of local business to search for (eg. "Restaurants").
 *  @input {number} latitude First part of coordinates for local business search. Defaults to UC Berkeley's coordinates.
 *  @input {number} longitude Second part of coordinates for local business search. Defaults to UC Berkeley's coordinates.
 *  @input trigger An input to trigger the search.
 *  @output {boolean} ready WARNING: Triggers may not work correctly before this output has been produced.
 *   This accessor produces true on this output when it has successfully loaded its API key
 *   and is ready for queries.
 *   If this accessor is acquring its API key asynchronously, this may occur after initialization has completed.
 *  @parameter {boolan} getAPIKeySynchronously Not all hosts support synchronous or asynchronous file reading.
 *   specify the right mode for this accessor's host here.
 *  @parameter {string} APIKey An authentication token provided by Yelp for using
 *   their API. See https://www.yelp.com/developers/documentation/v3/authentication.
 *  @parameter {number} expirationPeriod The duration of time (in milliseconds) from receiving the Yelp response in this accessor 
 *   after which the observation output on semanticObservation should no longer be considered accurate
 *   and is safe to remove from a semantic repository.
 *  @parameter {int} timeout The amount of time (in milliseconds) to wait for a response
 *   before triggering a null response and an error. This defaults to 5000.
 *  @output semanticObservation The YelpSearch results presented as a turtle ontology. 
 */


// Stop extra messages from jslint and jshint.  Note that there should
// be no space between the / and the * and global. See
// https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*globals addInputHandler, addInputParameter, console, error, exports, extend, input, get, getParameter, getResource, output, parameter, send */
/*jshint globalstrict: true*/
'use strict';
 
 /** Set up the accessor by defining the inputs and outputs.
 */

var N3 = require('n3');
var DataFactory = N3.DataFactory;
var quad = DataFactory.quad;
var literal = DataFactory.literal;
var namedNode = DataFactory.namedNode;
var store = new N3.Store();

exports.setup = function () {
    this.extend('services/YelpSearch');
    this.parameter('expirationPeriod', {
        'type': 'number',
        'value': 30000
    });
    this.output('response', {
        'visibility': 'expert'
    });
    this.output('semanticObservation', {
        'type': 'string'
    });
};

exports.initialize = function() {
    exports.ssuper.initialize.call(this);
}

//Overriding YelpSearch
exports.handleResponse = function(message){
    var thiz = this;

    var writer = N3.Writer({ prefixes: { sosa: 'http://www.w3.org/ns/sosa/',
                                        rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
                                        xsd: 'http://www.w3.org/2001/XMLSchema#',
                                        schema: 'http://schema.org/'
                                        }
                            });

    //The N3 createBlankNode function helps to create blank node names that are unique for the N3 instance in this accessor.
    //Other accessors producing ontologies for the same repository using different instances of N3
    //will not necessarilly be unique. Fortunately, GraphDB creates new unique blank node names and by default
    //scopes inserted blank nodes only within the file they are inserted from.
    var obsNode = store.createBlankNode('SemanticYelpSearchObservation');
    writer.addQuad(
      obsNode,
      namedNode('rdf:type'),
      namedNode('sosa:Observation')
    );
    var date = new Date();
    var formattedDate = date.toISOString();
    writer.addQuad(
      obsNode,
      namedNode('sosa:resultTime'),
      literal(formattedDate, namedNode('xsd:dateTime'))
    );
    
    //Schema.org uses custom datatypes for their ontologies. I've converted them to xsd types here.
    //This conversion is endorsed by the schema.org people themselves. See https://github.com/schemaorg/schemaorg/issues/1781
    //In that same thread, there is a good argument for replacing all instances of schema:date with xsd:dateTime. I did it.
    //Also, although I'm using schema:expires correctly here,
    //all instances from schema.org use this to refer to published works.
    var expirationDate = new Date(date.getTime() + thiz.getParameter('expirationPeriod'));
    var formattedExpires = expirationDate.toISOString(); 
    writer.addQuad(
      obsNode,
      namedNode('schema:expires'),
      //literal(formattedExpires, namedNode('schema:date'))
      literal(formattedExpires, namedNode('xsd:dateTime'))
    );

    //Parse yelp's json response. Each business is a sosa:Result
    if(message && message.body){
        var yelpData = JSON.parse(message.body);
        if(yelpData && yelpData.businesses){
            for (var i = 0; i < yelpData.businesses.length; i++) {
                var business = yelpData.businesses[i];
                var businessNode = store.createBlankNode('SemanticYelpSearchBusiness');
                var ratingNode = store.createBlankNode('SemanticYelpSearchRating');

                writer.addQuad(
                  obsNode,
                  namedNode('sosa:hasResult'),
                  businessNode
                );
                writer.addQuad(
                  businessNode,
                  namedNode('rdf:type'),
                  namedNode('schema:LocalBusiness')
                );
                writer.addQuad(
                  businessNode,
                  namedNode('schema:name'),
                  //literal(business.name, namedNode('schema:text'))
                  literal(business.name)
                );
                writer.addQuad(
                  businessNode,
                  namedNode('schema:telephone'),
                  //literal(business.display_phone, namedNode('schema:text'))
                  literal(business.display_phone)
                );
                if(business.location.display_address){
                    writer.addQuad(
                      businessNode,
                      namedNode('schema:address'),
                      //literal(business.location.display_address, namedNode('schema:text'))
                      literal(business.location.display_address)
                    );
                }
                //Order of a wkt CRS84 point is longitude, latitude
                if(business.coordinates && business.coordinates.latitude && business.coordinates.longitude){
                    writer.addQuad(
                      businessNode,
                      namedNode('http://www.opengis.net/ont/geosparql#asWKT'),
                      literal('<http://www.opengis.net/def/crs/OGC/1.3/CRS84> Point(' + business.coordinates.longitude + ' ' + business.coordinates.latitude + ')', namedNode('http://www.opengis.net/ont/geosparql#wktLiteral'))
                    );
                }
                writer.addQuad(
                  businessNode,
                  namedNode('schema:aggregateRating'),
                  ratingNode
                );
                writer.addQuad(
                  ratingNode,
                  namedNode('rdf:type'),
                  namedNode('schema:AggregateRating')
                );
                writer.addQuad(
                  ratingNode,
                  namedNode('schema:reviewCount'),
                  //literal(business.review_count, namedNode('schema:Integer'))
                  literal(business.review_count, namedNode('xsd:integer'))
                );
                writer.addQuad(
                  ratingNode,
                  namedNode('schema:ratingValue'),
                  //literal(business.rating, namedNode('schema:Number'))
                  literal(business.rating, namedNode('xsd:decimal'))
                );
            }
        }
    }

    writer.end(function(error, result) { 
        thiz.send('semanticObservation', result);
    });
    exports.ssuper.handleResponse.call(this, message);
}