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

// Copyright (c) 2016-2017 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 an address given a location.
 *  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 a key in $KEYSTORE/geoCoderKey, which
 *  resolves to $HOME/.ptKeystore/geoCoderKey (GeoCoding uses the same key as ReverseGeoCoding). 
 *
 *  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.
 *
 *  If multiple addresses are returned from the google reverse geocoding service,
 *  this accessor outputs the first one on address. The full response is available
 *  at the response output.
 *  
 *  The accuracy property of the location input is used to filter the returned results.
 *  If for example, location is given at a very low accuracy and the given coordinates are
 *  intended to represent an entire city or district, it would be overly specific to return
 *  the street address. Instead the name of the city should be the output.
 *
 *  @accessor services/ReverseGeoCoder
 *  @input location The location, as an object with a 'latitude' and 'longitude'
 *   property.
 *  @output {string} address The first returned address, for example "Berkeley, CA".
 *  @output response An object containing the full address information.
 *  @author Matt Weber
 *  @version $$Id: GeoCoder.js 1804 2017-06-02 19:23:00Z cxh $$
 */

// 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('location',{
        "type": 'JSON'
    });
    this.output('address',{
        "type": 'string'
    });

    // 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'
    });

    this.input('arguments', {
        'visibility': 'expert'
    });
    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'
    });
};

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;

    // Handle location information.
    this.addInputHandler('location', function () {
        var location = this.get('location');
        if (location) {
            // 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();
            } 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 = 'ThisIsNotAPipeNorIsItAWorkingKeySeeTheReverseGeoCoderAccessorDocs';
            }

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

            // arguments is a reserved word, so we use args.
            var args = {
                'latlng': location.latitude + ',' + location.longitude,
                'key': key
            };

            //Set a filter for the reverse geocoding request based on the accuracy of the
            //location object. Default to no filter if accuracy is not specified or set to high.
            //The list of results from google is ordered by accuracy. 
            if(location.accuracy && location.accuracy == "low"){
                args.result_type = "locality";
                //according to the google API https://developers.google.com/maps/documentation/geocoding/intro#Types
                //"locality indicates an incorporated city or town political entity."
                //I need to do more experimenting, but it seems ip based location is accurate
                //up to this level but no further. 

            }
            self.send('arguments', args);
            self.send('trigger', true);
        } else {
            throw 'ReverseGeoCoder: No location.';
        }
    });
};


/** Filter the response, extracting the addresses 
 */
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('ReverseGeoCoder: Unable to parse response: ' + err.message +
                    '\nResponse was: ' + response);
                // So that downstream actors don't just a previous location, send null.
                this.send('address', 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].formatted_address)  {
            this.send('address', parsed.results[0].formatted_address);
        } else {
            error('ReverseGeoCoder: No matching address.');
            // So that downstream actors don't just a previous location, send null.
            this.send('address', null);
        }
    }
    return response;
};