Accessing Maps from Google Apps script bound to form - google-apps-script

I’m trying to access Maps from a script bound to a Google form. The problem i’m having is that when debugging the script, it accesses Maps so often that i’m Running into quota limits. I have a Maps API key but do not have a client ID so can’t get Maps.setAuthenication(clientID,Key) to work. I’m doing this for a Scout Troop so don’t want to have to pay to access maps.
Can anyone help?
I was subsequently asked to post my code, and so here it is:
function setLocation(){
var whereString;
var theDuration;
var theDistance;
var theRoute;
var theDirections;
var theTravelString;
// this Sets the Where: Tab on the form
whereString = 'Where: ' + gLocation;
theItemArray = gSignupForm.getItems();
theItemArray[kWhereItem].setTitle(whereString);
//this gets the directions to the location
Maps.setAuthentication('','ABCDEFGHIJKLMNOP');
//Obviously Im'm not going to post the true key
theDirections = Maps.newDirectionFinder()
.setOrigin('7101 Shadeland Ave, Indianapolis, IN 46256')
.setDestination(gLocation)
.setMode(Maps.DirectionFinder.Mode.DRIVING)
.getDirections();
theRoute = theDirections.routes[0];
theDuration = theRoute.legs[0].duration.text;
theDistance = theRoute.legs[0].distance.text;
theTravelString = Utilities.formatString('Travel Considerations: The estimated travel distance is %u miles. ',theDistance);
theTravelString += 'The estimated travel time is ' + theDuration;
theItemArray[kTravelItem].setTitle(theTravelString);
}

One solution to limit the use of low-quota services is to avoid calling those services when the desired information has not changed.
For example, through the use of CacheService, you can dramatically reduce your calls to the Maps API, debugging session or not:
var cache = CacheService.getScriptCache();
function setLocation() {
// Try to find the route for this location if it's still available
var storedRoute = cache.get(gLocation);
if (!storedRoute) {
// No route for this value of the key gLocation was found. Query as normal.
...
theRoute = theDirections.route[0];
// Cache this route for future uses, for the maximum of 6hr).
cache.put(gLocation, JSON.stringify(theRoute), 21600);
} else {
// We have this exact stored route! Convert it from the stored string.
theRoute = JSON.parse(storedRoute);
}
theDuration = ...
...
Your "gLocation" variable may be directly usable as a cache key. If not, you'll need to make it useable by encoding it. The max length key is 250 characters. This single-parameter caching assumes your directions all have a fixed endpoint, as shown in your example code. If both endpoints vary, you'll have to construct a cache key based on both values.
Related question: Maps direction Quota limits

Related

How to appendRow (or write data more generally) to Google Sheets from a custom function

I've written a custom function [=ROUTEPLAN(origin,destination,mode,departuretime)] in the Google Sheets script editor. The function assigns a unique ID to the request, calls the Google Maps Directions API, passes as params the arguments as listed in the function, parses the JSON and extracts the duration, end latitude and end longitude for each step of the journey, and then appends a row for each step, with the request ID for the whole journey, the sequential step number, the duration, end latitude and end longitude:
function ROUTEPLAN() {
//Call the google route planner api
//(variables for api declared here but removed for brevity)
var routeResponse = UrlFetchApp.fetch("https://maps.googleapis.com/maps/api/directions/json?origin=" + origin
+ "&destination=" + destination
+ "&mode=" + mode +
"&region=uk&departure-time=" + departuretime
+ "&key=MYAPIKEY")
//Assign a unique ID to this request
var requestID = Date.now() + Math.random();
//Parse JSON from routeResponse
var json = routeResponse.getContentText();
var data = JSON.parse(json);
//Insert the RequestID, step number, duration, end Latitude and end Longitude for each step of the journey into the RouteDetails sheet
var steps = data["routes"][0]["legs"][0]["steps"];
for (i = 0; i < steps.length; i++) {
var stepID = i + 1;
var duration = steps[i]["duration"]["value"];
var endLat = steps[i]["end_location"]["lat"];
var endLng = steps[i]["end_location"]["lng"];
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("RouteDetails")
sheet.appendRow([requestID,stepID,duration,endLat,endLng]);
}
}
Or at least that's what I want it to do. It worked fine until I tinkered with it, and now I'm getting an ERROR when I call the function in the spreadsheet, telling me I don't have permission to call appendRow. I know why this is happening (although I don't understand why it wasn't happening before), but I cannot work out what I'm supposed to do about it.
If appendRow exists, there must be some circumstance in which it can be used to write data the sheet, but I can't figure out the circumstances in which permission to write to the sheet would be granted.
The purpose of the sheet is to provide data to a chatbot (the chatbot app has read & write permissions to the sheet). I'm not intending to provide access beyond that (i.e. i'm not intending to publish this for wider use). I've tried going down the installable trigger route, but despite following all the instructions that made absolutely no difference to the outcome. From the limited understanding I gained from reading about API Executables, that doesn't seem to be an option either.
Can anyone tell me how to solve this? Thank you :-)
A custom function can not modify the structure of the spreadsheet, so calling appendRow() is not allowed as stated in the documentation:
A custom function cannot affect cells other than those it returns a value to. In other words, a custom function cannot edit arbitrary cells, only the cells it is called from and their adjacent cells. To edit arbitrary cells, use a custom menu to run a function instead
If you want to return multiple rows from your function, it needs to return a two dimensional array. Note however that custom functions have the same limitation as native functions of not being able to overwrite content i.e. if you try to return two rows but the row below is already filled the function will error out.

Using Google Maps API to estimate traffic

I'm trying to call the Google Maps API to get the travel time between two points (including traffic). Here's what I've got so far:
function test(){
//Info: https://developers.google.com/maps/documentation/directions/intro
var baseUrl = "https://maps.googleapis.com/maps/api/directions/json?";
var origin = "Redmond+WA";
var destination = "Salem+OR";
var departureTime = "now";
var trafficModel = "pessimistic";
var url = baseUrl + "origin=" + origin + "&destination=" + destination + "&departure_time=" + departureTime + "&traffic_model=" + trafficModel;
var response = UrlFetchApp.fetch(url);
Logger.log("Google Maps API: " + JSON.parse(response).routes[0].legs[0].duration.text);
Logger.log("Full response from API: \n" + response);
}
Unfortunately, I always get a result of 3 hours 43 mins, no matter when I run the code (and despite the fact that I have defined departure_time and traffic_model. Any suggestions?
(On a side note, the documentation says that I need to pass in an API key as a required parameter. Obviously - per my code above - I haven't done this yet. But it didn't prevent me from getting a response. Could this be preventing me from using the traffic_model parameters?)
What field do you see: duration or duration_in_traffic?
According to the documentation
duration_in_traffic indicates the total duration of this leg. This value is an estimate of the time in traffic based on current and historical traffic conditions. See the traffic_model request parameter for the options you can use to request that the returned value is optimistic, pessimistic, or a best-guess estimate. The duration in traffic is returned only if all of the following are true:
The request includes a valid API key, or a valid Google Maps APIs Premium Plan client ID and signature.
The request does not include stopover waypoints. If the request includes waypoints, they must be prefixed with via: to avoid stopovers.
The request is specifically for driving directions—the mode parameter is set to driving.
The request includes a departure_time parameter.
Traffic conditions are available for the requested route.
https://developers.google.com/maps/documentation/directions/intro#DirectionsResponseElements
I understand you cannot see duration_in_traffic field, because you don't apply an API key.
Hope it helps!

Google Apps Script - How to get driving distance from Maps for two points in spreadsheet

Good evening,
I have a spreadsheet where I record my daily driving mileage when working. The column headings are: Date, Point A, Point B, Point C, Point D, Point E, Point F, Point G, and Trip Mileage.
Currently, I manually use Google Maps to determine driving distance between points. What I'd like to see happen instead is for a script to pull the points data from my spreadsheet, determine distance between the points and total distance of each trip (i.e. row), and insert the total in the last column.
I believe I have this essentially all set up except I can't figure out how to get the distance data from Google Maps. From reading similar question, I sense someone might tell me to refer to the Maps Distance Matrix API. I've referred to this. I'm a beginner programmer, but it appears to me to be oriented toward apps, not Google docs. Is this the case? Or can it be made to serve my project?
Thank you in advance for all of your wonderful advice, comments, suggestions, and encouragement!!!
BTW, here is my code currently:
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mileageLogSheet = ss.getSheetByName("Mileage Log");
var dataRange = mileageLogSheet.getDataRange(); //Returns a Range corresponding to the dimensions in which data is present.
var numRows = dataRange.getNumRows();
var numCol = dataRange.getNumColumns(); //numCol = 9.
//Returns the range with the top left cell at the given coordinates (row, column) with the given number of rows and columns.
var rangeToCompute = mileageLogSheet.getRange(10, 2, numRows-9, numCol-1);
var address1,
address2;
for(var rowCoord = 1; rowCoord < numRows-9; rowCoord++){
for(var colCoord = 1; colCoord < 8; colCoord++){
if(rangeToCompute.getCell(rowCoord, colCoord).getValue() != ""){
address1 = rangeToCompute.getCell(rowCoord, colCoord).getValue();
address2 = rangeToCompute.getCell(rowCoord, colCoord+1).getValue();
var directions = Maps.newDirectionFinder()
.setOrigin(address1)
.setDestination(address2)
.setMode(Maps.DirectionFinder.Mode.DRIVING)
.getDirections();
//Nope, this doesn't work!:
var distanceValue = directions.routes[0].legs[0].distance.value;
var totalTripDistance;
}
}
}
}
You can take use of the Google Maps WEB API's Direction API. Which give you a detail result on direction from point A to point B, which include the distance information you want.
To get started, you need an API KEY for using the Google Maps Direction API. Go to console.developers.google.com to create an account, create a project, enable the Directions API in your project and then create a credential (API KEY). You should able to create it as server API key and give it the address 0.0.0.0/0, which means all computer on earth, as long as you keep this project to yourself. For more information on creating API KEYS, you can take a look at this section.
Then in the spreadsheets, you need to first create the request URL by CONCAT your key, origin and destination. Here is how I did it:
=CONCAT(CONCAT(CONCAT(CONCAT(CONCAT("https://maps.googleapis.com/maps/api/directions/json?origin=",SUBSTITUTE(B2," ","+")),"&destination="),SUBSTITUTE(C2," ","+")),"&key="),$A$2)
*Note that you need to SUBSTITUTE spaces " " to pluses "+"
Then under the Tools > Script editor..., create an empty script and replace everything with the following code, and save:
function getDistance(url) {
var response = UrlFetchApp.fetch(url);
var json = response.getContentText();
var data = JSON.parse(json);
var distance = data.routes[0].legs[0].distance.text;
return distance;
}
This is a customized function that parse the Google Maps Direction API JSON result and return the distance information. The JSON usually contains multiple routes and under the first route- routes[0], exist multiple legs and in the first leg- legs[0], exist a distance, which include a text and value field, which are a human readable field, and a integer number that represent the distance in meter.
return to the spreadsheets and you can call =getDistance(CellWithURL) to get the distance between two places.
I created an example here, hope it helps.
**ps. somehow it works without the API KEY too.. but you should apply for one for otherwise you violate the ToS.

Set Maps API key in Script Editor

As far as I understand, in order to track our quota usage, we need to provide our API key to the Google App Service on the service we are planning to use.
In my case I have a spreadsheet with Origin and Destination and a Custom function to calculate the distance between.
I ran into the problem of meeting the quota from invoking .getDirections():
Error: Service invoked too many times for one day: route. (line **).
Sample of the code:
function getDirections_(origin, destination) {
var directionFinder = Maps.newDirectionFinder();
directionFinder.setOrigin(origin);
directionFinder.setDestination(destination);
var directions = directionFinder.getDirections();
return directions;
}
So I read that if I assign the API Key to my project I should be able to see the usage and how close to the free quota I am.
In the script editor, I did enable all of the APIs under Resources menu/ Advanced Google Services. Then I went to the Google Developers Console and there
I did not see any record of how many times my custom function called the Google Maps API or any API usage.
Logically I think that in my script I need to set my google API Key so my scripts start to call the API under my user name and count the number of time I used certain API. I guess right now I am using the Google Maps API as anonymous and since the whole company is assigned with the same IP, so we exhaust the permitted numbers to call this function.
Bottom line please reply if you know a way to connect my simple Spreadsheet function to the Public API access Key I have.
Thank you,
Paul
I also have been eager to find this answer for a long time and am happy to say that I've found it. It looks like Google might have just made this available around Oct 14, 2015 based on the date this page was updated.
You can leverage the UrlFetchApp to add your API key. The link I posted above should help with obtaining that key.
function directionsAPI(origin, destination) {
var Your_API_KEY = "Put Your API Key Here";
var serviceUrl = "https://maps.googleapis.com/maps/api/directions/json?origin="+origin+"&destination="+destination+
"&mode="+Maps.DirectionFinder.Mode.DRIVING+"&alternatives="+Boolean(1)+"&key="+Your_API_KEY;
var options={
muteHttpExceptions:true,
contentType: "application/json",
};
var response=UrlFetchApp.fetch(serviceUrl, options);
if(response.getResponseCode() == 200) {
var directions = JSON.parse(response.getContentText());
if (directions !== null){
return directions;
}
}
return false;
}
So walking through the code... first put in your API key. Then choose your parameters in the var serviceUrl. I've thrown in additional parameters (mode and alternatives) to show how you can add them. If you don't want them, remove them.
With UrlFetch you can add options. I've used muteHttpExceptions so that the fetch will not throw an exception if the response code indicates failure. That way we can choose a return type for the function instead of it throwing an exception. I'm using JSON for the content type so we can use the same format to send and retrieve the request. A response code of 200 means success, so directions will then parse and act like the object that getDirections() would return. The function will return false if the UrlFetch was not successful (a different response code) or if the object is null.
You will be able to see the queries in real time in your developer console when you look in the Google Maps Directions API. Be sure that billing is enabled, and you will be charged once you exceed the quotas.
1.) I added an API key from my console dashboard. Remember to select the correct project you are working on. https://console.developers.google.com/apis/credentials?project=
2.) In my Project (Scripts Editor) I setAuthentication to Maps using the API key and the Client ID from the console. I have included the script below:
function getDrivingDirections(startLoc, wayPoint, endLoc){
var key = "Your_API_Key";
var clientID = "Your_Client_ID";
Maps.setAuthentication(clientID, key);
var directions = Maps.newDirectionFinder()
.setOrigin(startLoc)
.addWaypoint(wayPoint)
.setDestination(endLoc)
.setMode(Maps.DirectionFinder.Mode.DRIVING)
.getDirections();
} return directions;
https://developers.google.com/apps-script/reference/maps/maps#setAuthentication(String,String)
As of 7/13/2017, I was able to get the API to function by enabling the Sheets API in both the "Advanced Google Services" menu (images 1 and 2), and in the Google Developer Console. If you're logged into Google Sheets with the same email address, no fetch function should be necessary.
[In the Resources menu, select Advanced Google Services.][1]
[In Advanced Google Services, make sure the Google Sheets API is turned on.][2]
[1]:
[2]:
Thank goodness for JP Carlin! Thank you for your answer above. JP's answer also explains his code. Just to share, without a code explanation (just go look above for JP Carlin's explanation), below is my version. You will see that I also have the departure_time parameter so that I will get distance and driving-minutes for a specific date-time. I also added a call to Log errors (to view under "View/Logs"):
Note: Google support told me that using your API-key for Google Maps (e.g. with "Directions API") with Google Sheets is not supported. The code below works, but is an unsupported work-around. As of 11/4/2018, Google has an internal ticket request to add support for Google Maps APIs within Google Sheets, but no timeline for adding that feature.
/********************************************************************************
* directionsAPI: get distance and time taking traffic into account, from Google Maps API
********************************************************************************/
function directionsAPI(origin, destination, customDate) {
var Your_API_KEY = "<put your APK key here between the quotes>";
var serviceUrl = "https://maps.googleapis.com/maps/api/directions/json?origin="+origin+"&destination="+destination+"&departure_time="+customDate.getTime()+"&mode="+Maps.DirectionFinder.Mode.DRIVING+"&key="+Your_API_KEY;
var options={
muteHttpExceptions:true,
contentType: "application/json",
};
var response=UrlFetchApp.fetch(serviceUrl, options);
if(response.getResponseCode() == 200) {
var directions = JSON.parse(response.getContentText());
if (directions !== null){
return directions;
}
}
Logger.log("Error: " + response.getResponseCode() + " From: " + origin + ", To: " + destination + ", customDate: " + customDate + ", customDate.getTime(): " + customDate.getTime() );
return false;
}

Google Maps API - Finding Waypoint That Caused Error

We have been developing an ASP.NET application that works with the Google Maps API to assist in logistics and planning for a small shipping company in our area. One of the features is for the company to input a list of customers and the system will package up all the customer addresses as a series of waypoints, send it off to Google, and get back the direction information for the route.
One problem that we have been having is that many of the addresses in our database are not great and will often times are not able to be processed by Google Maps. When we do this we receive back an error code, but I would like to be able to determine which waypoint was the one to fail (that way the client can then go and fix the address in the database).
EDIT Here is a chunk of the code that handles the initialization and current error handling:
function initialize() {
if (GBrowserIsCompatible()) {
map = new GMap2(document.getElementById("map"));
map.addControl(new GSmallMapControl());
gdir = new GDirections(map);
GEvent.addListener(gdir, "load", onGDirectionsLoad);
GEvent.addListener(gdir, "error", handleErrors);
if (document.getElementById("<%=hiddenWayPoints.ClientID %>").getAttribute("value") != '')
{
setDirections();
}
}
}
function setDirections() {
var waypoints = new Array();
var str = document.getElementById("<%=hiddenWayPoints.ClientID%>").getAttribute("value");
waypoints = document.getElementById("<%=hiddenWayPoints.ClientID %>").getAttribute("value").split(":");
gdir.loadFromWaypoints(waypoints, {getSteps:true});
}
function handleErrors(){
if (gdir.getStatus().code == G_GEO_UNKNOWN_ADDRESS)
alert("No corresponding geographic location could be found for one of the specified addresses. This may be due to the fact that the address is relatively new, or it may be incorrect.\nError code: " + gdir.getStatus().code);
else if (gdir.getStatus().code == G_GEO_SERVER_ERROR)
alert("A geocoding or directions request could not be successfully processed, yet the exact reason for the failure is not known.\n Error code: " + gdir.getStatus().code);
else if (gdir.getStatus().code == G_GEO_MISSING_QUERY)
alert("The HTTP q parameter was either missing or had no value. For geocoder requests, this means that an empty address was specified as input. For directions requests, this means that no query was specified in the input.\n Error code: " + gdir.getStatus().code);
else if (gdir.getStatus().code == G_GEO_BAD_KEY)
alert("The given key is either invalid or does not match the domain for which it was given. \n Error code: " + gdir.getStatus().code);
else if (gdir.getStatus().code == G_GEO_BAD_REQUEST)
alert("A directions request could not be successfully parsed.\n Error code: " + gdir.getStatus().code);
else alert("An unknown error occurred.");
}
The problem is that the LoadFromWayPoints() method in the GDirections object doesn't seem to return status based on each individual waypoint, but from just the entire list (if one fails it all fails).
The only real solution I can think of (without performing checks in other areas of the system) is to send each waypoint off to Google Maps in a separate request and check it's validity that way prior to sending off the entire waypoints list for the GDirections object, but that seems incredibly inefficient (especially when dealing with a larger set of customer locations).
If you are using the GClientGeocoder object to do your requests, then you will get back an appropriate response code for each getLocations call:'
function logAddress (response)
{
if (!response || response.Status.code != 200)
{
// log the response.name and the response.Status.code
return;
}
// otherwise everything was fine
}
for (var i = 0; i < addresses.length; i++)
{
var geocoder = new GClientGeocoder ();
geocoder.getLocations (addresses[i], logAddress);
}
Their are various response codes, but I am guessing you want to inform the user when you get a 602 - Unknown Address.
EDIT:
Yep, you will only get a single error callback for loadFromWaypoints for the entire directions request. What you are doing is more than just a simple geocoding request, you are actually generating directions and rendering overlays to a map for a sequence of addresses values. I suggest a couple of solutions:
You could do a getLocation request before your loadFromWaypoints request (as you suggested) and then use the latitude,longitude data returned for each address as the parameter for your loadFromWayPoints. This splits the geocoding processing out of the loadFromWayPoints request, but adds the extra round trip for each geocoding lookup. This is really something you only want to do once, when the user first enters the address (see next).
When the user enters the address information for the first time you can do a GClientGeocoder getLocations lookup at the time and get the latitude,longitude to store in your database along with the address. That way, if the user enters an address that can't be geocoded, then you can ask them to re-enter the address, or perhaps let them select the location on a google map (and get the lat,lng from that). This doesn't solve the problems with address data you have now, but perhaps you can write some code to run through you existing data (offline) and flag the addresses in the db that are not able to be geocoded.