Using Google Maps API with Google cloud functions


If you are thinking about using Google Cloud functions (and you should), you may be wondering how to use things like Google Maps in the context of cloud functions. Well, cloud functions allow you to run Node.js without the need for the server, so that means that anything you can create in Node, you can run as a cloud function.

Of course Node doesn't have a DOM, so that means that the usual versions of the MAPS Api don't work - but there is a little known Node version of Maps available too, that allow you to do geocoding and so on - in fact most of the things you'd probably want to do in a sever like environment. Some of these are available in Apps Script too of course, but if you want to save your quotas, or integrate with other Node functions, you can use Cloud functions. 

The code for this function is on github

MAPS

You'll get that here, and you can install it with NPM. In this example I'm going to generate random places within a polygon with represents a given area. There are some functions in MAPS that allow you figure out if a point is within a polygon, but not in the Node version, so I need to use something else - in this case I'll be using point-in-polygon. Once I've installed these into Node, we're good to go

  var pp = require('point-in-polygon');
  ns.maps = require('@google/maps');

but I'll also install the cloud functions emulator which allows me to test everything locally before deploying the cloud function formally.

Ephemeral Exchange

This cloud function is actually a microservice in an ecosystem, which gets its instructions and publishes its results by subscribing to an item in a cross platform cache. Now and again it will be asked to generated a random place, and it will respond by publishing the updated item. Other services in the ecosystem will be listening for that change too and will do things with each of the places generated. You can get the node client for that here if you want.

I won't go into all the code here, but will publish the entire thing at a later time. For now I'm just going to go into the main points. 

Generating the polygon

I took this from a public GeoJson file which contains amongst other things, a list of points that describe the polygon.
   "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-0.071151117801821, 51.51029343697843],
            [-0.07113642319992, 51.5101295110986],
            [-0.071146828383437, 51.50984908079782],
            [-0.071168171767067, 51.50978737577289],  ....

I convert these and write them to cache to be shared by all co-operating processes in the ecosystem
{
  "name": "pd-demo",
  "polygon": [
    {
      "lng": -0.071151117801821,
      "lat": 51.51029343697843
    },
    {
      "lng": -0.07113642319992,
      "lat": 51.5101295110986
    }, ....

One of the processes wakes up and maps this area when it is detected, like this

Generating a random place

The way I'm approaching this is to generate a random longitude and latitude with the area by first getting the most north easterly and south westerly points of the polygon, and randomly generating points within those limits
  // set the bounds max & mins
    ns.data.limits = {
      sw: poly.reduce(function(p, c) {
        return typeof p.lat === typeof undefined ? c : {
          lat: Math.min(p.lat, c.lat),
          lng: Math.min(p.lng, c.lng)
        };
      }, {}),
      ne: poly.reduce(function(p, c) {
        return typeof p.lat === typeof undefined ? c : {
          lat: Math.max(p.lat, c.lat),
          lng: Math.max(p.lng, c.lng)
        };
      }, {}),
    };

and keep eliminating edge points that fall outside the polygon.
  function getPoint_() {
    for (var p = 0; p < ns.settings.maxTries; p++) {
      var point = {
        lat: Math.random() * (ns.data.limits.ne.lat - ns.data.limits.sw.lat) + ns.data.limits.sw.lat,
        lng: Math.random() * (ns.data.limits.ne.lng - ns.data.limits.sw.lng) + ns.data.limits.sw.lng
      };
      if (ns.inside(point)) return point;
    }
    throw 'unable to get a random point after ' + ns.settings.maxTries + ' times ';
  }

ns.inside uses point-in-polygon 
  ns.inside = function(point) {
    return pp(ns.llToArray(point), ns.data.ll);
  };

which uses an array for lat/lng rather than a literal so I need to convert
  ns.llToArray = function(ll) {
    return [ll.lng, ll.lat];
  };

Eventually then, I find a point that is wholly inside the polygon, and now I need to find a place close by. I'm actually looking for 'bars', but it could be any other type of place too. This is where we use the Node version of the Maps API, so first we need to create a client.
   ns.data.googleMapsClient = ns.maps.createClient({
      key: keys.mapsApiKey,
      Promise: Promise
    });

Now I can get the random point , and find a nearby bar. I have to run it through ns.inside again, because the place maybe close to the given point, but may actually be outside the polygon. Since the function returns them in order of closeness, I stop when I've found a place still inside the polygon.
 /**
   * get a random point inside the poly
   */
  ns.getPoint = function() {
    var point = getPoint_();

    var request = {
      language: 'en',
      location: point,
      rankby: 'distance',
      type: ns.data.keyRow['places-type']
    };

    return ns.data.googleMapsClient.placesNearby(request)
      .asPromise()
      .then(function(places) {
        // i've only taken the first default page
        // if there's nothing there then just pass on this one
       
        // pick the first one still in the zone
        if (places.json.status === "OK") {

          return places.json.results.reduce(function(p, c) {

            if (!p && c.vicinity && ns.inside(c.geometry.location)) {
              p = {
                "icon": c.icon,
                "name": c.name,
                "place_id": c.place_id,
                "vicinity": c.vicinity,
                "photos": c.photos,
                "lat":c.geometry.location.lat,
                "lng":c.geometry.location.lng
              };
             
            }
            return p;
          }, null);
        }
        else {
          return null;
        }
      });


  };

And that's it, all that remains is to write the thing to cache, and anybody who cares will have a new place added. Here's the entire cloud function. 

exports.pdordergenerator = function(req, res) {
  if (req.body.contents === undefined) {
    // This is an error case, as "message" is required
    res.status(400).send('No message defined!');
  }
  else {

    // initialize the thing
    var efx = Exchange.init(req.body.contents).handle;

    // and set the session to this app name for future info
    efx.setSession("pd-order-generator");
    var keys = efx.getKeys();

    // now read the given item, with an intention to update, and also activate exp backoff 
    efx.read(keys.item, keys.updater, {
        "intention": "update"
      },true)
      .then(function(result) {
        // kick off making a new point
        if (!result.data.ok) throw result.data;
        MakePoint.init(result.data.value, keys);
        return Promise.all([MakePoint.getPoint(), Promise.resolve(result.data)]);
      })
      .then(function(result) {
        // take the first resukts
        var data = result[1];
        var item = result[0];
        console.log('item', item);
        
        if (item) {
          // all that happens is that no random order is created if cant get a point
          data.value.points.push(item);

          // write back to the thing
          return efx.update(data.value, keys.item, keys.updater, "post", {
            intent: data.intent
          });
        }
        else {
          return {
            data: data
          };
        }
      })
      .then(function(result) {
        if (!result.data.ok) throw result.data;
        res.status(200).end();
      })
      .catch(function(err) {
        console.log('err', err);
        res.status(500).end();
      });


  }

};

// namespace for making a point
var MakePoint = (function(ns) {

  var pp = require('point-in-polygon');
  ns.maps = require('@google/maps');


  ns.data = {};
  ns.settings = {
    maxTries: 100
  };


  // set up the polygon that we're working with
  ns.init = function(value, keys) {

    // point in polygon uses a different format
    var poly = value.polygon;
    ns.data.ll = poly.map(function(d) {
      return ns.llToArray(d);
    });
    ns.data.poly = poly;
    ns.data.keyRow = value.keys;

    // set the bounds max & mins
    ns.data.limits = {
      sw: poly.reduce(function(p, c) {
        return typeof p.lat === typeof undefined ? c : {
          lat: Math.min(p.lat, c.lat),
          lng: Math.min(p.lng, c.lng)
        };
      }, {}),
      ne: poly.reduce(function(p, c) {
        return typeof p.lat === typeof undefined ? c : {
          lat: Math.max(p.lat, c.lat),
          lng: Math.max(p.lng, c.lng)
        };
      }, {}),
    };

    ns.data.googleMapsClient = ns.maps.createClient({
      key: keys.mapsApiKey,
      Promise: Promise
    });

  };

  /** convert {lat:,lng} to an array
   */
  ns.llToArray = function(ll) {
    return [ll.lng, ll.lat];
  };

  /**
   * get a random point inside the poly
   */
  ns.getPoint = function() {
    var point = getPoint_();

    var request = {
      language: 'en',
      location: point,
      rankby: 'distance',
      type: ns.data.keyRow['places-type']
    };

    return ns.data.googleMapsClient.placesNearby(request)
      .asPromise()
      .then(function(places) {
        // i've only taken the first default page
        // if there's nothing there then just pass on this one
       
        // pick the first one still in the zone
        if (places.json.status === "OK") {

          return places.json.results.reduce(function(p, c) {

            if (!p && c.vicinity && ns.inside(c.geometry.location)) {
              p = {
                "icon": c.icon,
                "name": c.name,
                "place_id": c.place_id,
                "vicinity": c.vicinity,
                "photos": c.photos,
                "lat":c.geometry.location.lat,
                "lng":c.geometry.location.lng
              };
            }
            return p;
          }, null);
        }
        else {
          return null;
        }
      });


  };

  function getPoint_() {
    for (var p = 0; p < ns.settings.maxTries; p++) {
      var point = {
        lat: Math.random() * (ns.data.limits.ne.lat - ns.data.limits.sw.lat) + ns.data.limits.sw.lat,
        lng: Math.random() * (ns.data.limits.ne.lng - ns.data.limits.sw.lng) + ns.data.limits.sw.lng
      };
      if (ns.inside(point)) return point;
    }
    throw 'unable to get a random point after ' + ns.settings.maxTries + ' times ';
  }

  /**
   * makebounds- dont have access to the geometry functions in maps, so use this
   * @param 
   */
  ns.inside = function(point) {
    return pp(ns.llToArray(point), ns.data.ll);
  };

  return ns;

})({});


// namespace for efx conversations
var Exchange = (function(ns) {

  // open efx
  ns.handle = require('../../effex-api-client/dist/index');
  ns.settings = {
    instance: 'dev'
  };

  // initialize for conversation with store
  ns.init = (content) => {

    // pick the instance
    ns.handle.setEnv(ns.settings.instance);

    // set the keys
    ns.handle.setKeys({
      id: content.id,
      alias: content.alias,
      updater: content.message && content.message.updater,
      item: content.item || content.alias || content.id,
      mapsApiKey: content.message && content.message.mapsApiKey
    });


    return ns;

  };


  return ns;
})({});






For more like this, see React, redux, redis, material-UI and firebase. Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.
Comments