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

// Copyright (c) 2015-2016 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/** Retrieve a location given an address.
 *  The location is given as an object with two numeric fields,
 *  "latitude" and "longitude". For example,
 *  `{"latitude": 37.85, "longitude": -122.26}` is
 *  the location of Berkeley, California.
 *
 *  This accessor requires a "key" for the Google Geocoding API, which you can
 *  obtain for free at https://developers.google.com/maps/documentation/geocoding/intro .
 *
 *  This accessor looks for key in $KEYSTORE/geoCoderKey, which
 *  resolves to $HOME/.ptKeystore/geoCoderKey.
 *
 *  This accessor does not block waiting for the response, but if any additional
 *  *address* input is received before a pending request has received a response
 *  or timed out, then the new request will be queued and sent out only after
 *  the pending request has completed. This strategy ensures that outputs are
 *  produced in the same order as the input requests.
 *
 *  @accessor services/GeoCoder
 *  @input {string} address The address, for example "Berkeley, CA".
 *  @output location The location, as an object with a 'latitude' and 'longitude'
 *   property.
 *  @output response An object containing the location information.
 *  @author Edward A. Lee
 *  @version $$Id$$
 */

// 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, console, get, getParameter, getResource, error, exports, extend, get, input, output, parameter, require, send */
/*jshint globalstrict: true*/
'use strict';

/** Set up the accessor by defining the inputs and outputs.
 */
exports.setup = function () {
    this.extend('net/REST');
    this.input('address');
    this.output('location');

    // Change default values of the base class inputs.
    // Also, hide base class inputs, except trigger.
    // Note the need for quotation marks on the options parameter.
    this.input('options', {
        'visibility': 'expert',
        'value': '{"url": "https://maps.googleapis.com"}'
    });
    this.input('command', {
        'visibility': 'expert',
        'value': 'maps/api/geocode/json'
    });
    // NOTE: The value can be given as a JSON string or a JavaScript object.
    this.input('arguments', {
        'visibility': 'expert',
        'value': {
            "address": "Berkeley, CA",
            "key": "Enter Key Here"
        }
    });
    this.input('body', {
        'visibility': 'expert'
    });
    this.input('trigger', {
        'visibility': 'expert'
    });
    this.output('headers', {
        'visibility': 'expert'
    });
    this.output('status', {
        'visibility': 'expert'
    });
    this.parameter('outputCompleteResponseOnly', {
        'visibility': 'expert'
    });
};

this.noGeoCoderKey = true;

exports.initialize = function () {
    // Be sure to call the superclass so that the trigger input handler gets registered.
    exports.ssuper.initialize.call(this);

    var self = this;
    this.noGeoCoderKey = true;
    // Handle location information.
    this.addInputHandler('address', function () {
        var address = this.get('address');
        if (address) {
            // The key from https://developers.google.com/maps/documentation/geocoding/intro
            var key = '';

            // See the accessor comment for how to get the key.
            var keyFile = '$KEYSTORE/geoCoderKey';
            try {
                key = getResource(keyFile, 1000).trim();
                this.noGeoCoderKey = false;
            } catch (e) {
                console.log('GeoCoder.js: Could not get ' + keyFile + ":  " + e +
                            '\nThe key is not public, so this accessor is only useful ' +
                            'If you have the key.  See ' +
                            'https://www.icyphy.org/accessors/library/index.html?accessor=services.GeoCoder');
                key = 'ThisIsNotAPipeNorIsItAWorkingKeySeeTheGeoCoderAccessorDocs';
            }

            // console.log('GeoCoder: address: ' + address + ' key: ' + key);

            // arguments is a reserved word, so we use args.
            var args = {
                'address': address,
                'key': key
            };
            self.send('arguments', args);
            self.send('trigger', true);
        } else {
            throw 'GeoCoder: No address.';
        }
    });
};

/** Filter the response, extracting the latitude and longitude and
 *  formatting.
 */
exports.filterResponse = function (response) {
    if (response) {
        // Note that for some hosts, the response is a string, needing to parsed,
        // and for some, its already been parsed.
        var parsed = response;
        if (typeof parsed === 'string') {
            try {
                parsed = JSON.parse(response);
            } catch (err) {
                error('GeoCoder: Unable to parse response: ' + err.message +
                    '\nResponse was: ' + response);
                // So that downstream actors don't just a previous location, send null.
                this.send('location', null);
            }
        }
        // NOTE: All of the following should be replaced with a generic
        // schema transformation utility.
        // FIXME: Just taking the first result if there are multiple matches.
        if (parsed.results &&
            parsed.results[0] &&
            parsed.results[0].geometry &&
            parsed.results[0].geometry.location &&
            parsed.results[0].geometry.location.lat &&
            parsed.results[0].geometry.location.lng) {
            this.send('location', {
                "latitude": parsed.results[0].geometry.location.lat,
                "longitude": parsed.results[0].geometry.location.lng
            });
        } else {
            var message = 'GeoCoder: No matching location.';
            if (this.noGeoCoderKey) {
                message += '  Could not find key in $KEYSTORE/geoCoderKey.  See stdout and https://www.icyphy.org/accessors/library/index.html?accessor=services.GeoCoder';
            }
            error(message);
            // So that downstream actors don't just a previous location, send null.
            this.send('location', null);
        }
    }
    return response;
};