1

Is there a way of obtaining the cities a route traced by DirectionsService.route() goes through?

For example, in the route in https://goo.gl/maps/trHkPUNzuDFEjYT27 , roads belonging to the cities of Sao Paulo (starting point), Anhanguera, Cajamar, Jundiai, (others...) and Campinas (ending point).

If we input the starting and ending point in the DirectionsService.route() method, we obtain a list of legs, which includes the road, mileage, and time to travel, but not the cities they belong to.

Is there a way of obtaining this data without calling additional API's ? Cost is an important issue when considering Maps API.

EDIT: Clarified that solution should not involve additional calls. This solution is not much better than calling PlacesService for each leg of the route, since it merely boxes parts of the route, and calls them anyways.

Eric Wu
  • 908
  • 12
  • 35
  • 1
    You can reverse-geocode each step (or point you are interested in) and extract the information from the geocoder response. – MrUpsidown Jul 23 '19 at 14:22
  • That would be awfully cost (and computationally) expensive, since each request to google api is paid. – Eric Wu Jul 23 '19 at 14:27
  • 2
    Yes I know. And further to that the geocoder is rate limited. But *as you said* the information is not part of the Directions response so... what else do you want to do? – MrUpsidown Jul 23 '19 at 14:29
  • I'd like to know if there are other parameters that can be used, or other services that can be called. – Eric Wu Jul 23 '19 at 14:37
  • Google Maps API's doc is remarkably poor when talking about hidden parameters. – Eric Wu Jul 23 '19 at 14:37
  • 1
    possible duplicate of [Get stopover towns using the Google Maps API (directions)](https://stackoverflow.com/questions/14641899/get-stopover-towns-using-the-google-maps-api-directions) – geocodezip Jul 23 '19 at 21:15
  • I understand the dupe vote, but why off-topic? – Eric Wu Jul 24 '19 at 13:07
  • BTW, the solution in the link provided by @geocodezip is also heavily tolled. `Nearby Search and Text Search return all of the available data fields for the selected place`. – Eric Wu Jul 24 '19 at 13:08
  • Not sure what you mean by "hidden parameters". Undocumented parameters should, in general, not be used, as they may change or go away at any time. Asking for recommendations of other services/APIs is off-topic for StackOverflow. If the data you are looking for is not in the DirectionsService response, you can't get it without using additional calls to Google Maps (and if there were a way to request additional information in the response, it would cost more). – geocodezip Aug 08 '19 at 21:23
  • By "Hidden parameters", I would mean "parameters that _are_ documented and expected to be used, but won't appear in the current version of the docs". That being said, I did not ask for recommendations of other services, I asked if there were calls that could be used for this purpose. I understand that DirectionsService would not return these data (otherwise this question would not be relevant), and that additional _call_ must be requested to retrieve them, but I want to avoid _repetitive, multiple_ calls inside Google Maps. – Eric Wu Aug 08 '19 at 22:48
  • @EricWu are you running front-end JS or Node? May I ask what server you are running, if it may help in the answer to do the processing via backend code instead of front-end JS? – Geza Kerecsenyi Aug 10 '19 at 08:51
  • @GezaKerecsenyi, I'm running front-end JS. How would you approach the backend proccessing? – Eric Wu Aug 27 '19 at 17:43

1 Answers1

1

My suggestions would be simply to abandon the approach of using the Google API for everything. Undoubtedly, it's the best navigation tool, and it's for this reason it's that expensive. So, I'd suggest to use some other method for the geocoding that's not through Google, especially if you're only looking for big cities (as is appears in your example). There's some 'free' APIs that already exist (in truth, they're usually never really free) - I'd only suggest this if you're serverless. In that case, I'd go with Nominatim - it has no limit caps (kind of, see operations.osmfoundation.org/policies/nominatim - you can spam it, but it's discouraged), no API keys, and is completely free - the only issue, of course, is that as you mentioned you'd have to go through each point and make a request to an API, which would take a lot of time. However, I'd do:

let zoom = 12; // Adjust to your needs: see nominatim.org/release-docs/develop/api/Reverse. Higher numbers will result in more places being matched, while lower numbers will result in faster execution.

let coords = [];

const stepList = [];

Array.from(googleResponse.routes[0].legs).forEach(leg => {
  stepList.push(...leg.steps);
});

stepList.forEach(e => {
  coords.push([e.endLocation.lat, e.endLocation.long]);
});

coords.push([legList[0].startLocation.lat, legList[0].startLocation.long]);

let arr = [];
let promises = [];
let bboxes = [];

const loopOn = (i, cb) => {
  const coord = coords[i];
  const nextLoop = () => {
    i+1 < coords.length? loopOn(i+1, cb) : cb();
  }

  let makeRequest = true;
  for (let bbox of bboxes) {
    if (coord[0] >= bbox[0] 
        && coord[0] <= bbox[1] 
        && coord[1] >= bbox[2] 
        && coord[1] <= bbox[3]){ // If it is within a bounding box we've already seen
          makeRequest = false; // there's no need to geocode it, since we already roughly know it's in an area we have already saved.
          break;
        }
  }

  if (makeRequest){
    var request = $.ajax({
      url: `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coord[0]}&lon=${coord[1]}&zoom=${zoom}`,
      type:'GET',
      dataType: 'jsonp',
      success: resp => {
        thisPlace = resp.address.city || resp.address.town || resp.address.village;
        thisPlace && arr.indexOf(thisPlace) === -1 && arr.push(thisPlace);
        bboxes.push(resp.boundingbox);
        nextLoop();
      }
    });
  } else {
    nextLoop();
  }
};

loopOn(0, () => {
  /*The rest of your code*/
});

This code simply goes through each leg (where I'm assuming googleResponse is the unfiltered but JSONified response from the Directions API, and requests it from Nominatim. I've made it a tad bit more efficient using Nominatim's bounding boxes, which return the rectangle around each city/village area, so we don't need to make a request if an instruction/step is literally simply to turn a corner in the same square/suburb/city district/city (this can be defined using the zoom variable).

The problem with this is that Nominatim, being free and quite unoptimised, is obviously not the fastest API out there. Even if Google's servers ran on a slow connection, they'd still be faster simply because they've optimised their product to run faster, using some low-level code. Meanwhile, Nominatim simply does a basic lookup from a file (no rainbow hashing, etc.), so it has to manually narrow down the area.

The solution would be to use a custom dataset. Obviously, this would require a backend to store it on, since downloading the entire CSV to the frontend with each load would take literal hours (and on every reload!). All you'd really need to do for this is replace the AJAX API request with a call to the csv-parser module (or any other parsing function), which works in much the same way regarding promises/async, so you could literally just replace the code with the example on their website:

let resp = [];

fs.createReadStream(<your file here.csv>)
  .pipe(csv())
  .on('data', (data) => resp.push(data))
  .on('end', () => {
    results = search(resp, coords);
    thisPlace = results.address.city || results.address.town || results.address.village;
    thisPlace && arr.indexOf(thisPlace) === -1 && arr.push(thisPlace);
    nextLoop();
  });

Also, you could remove the bounding-box code, since you don't need to save request time anymore.

However, rearranging it like so would be faster:

let resp = [];

fs.createReadStream(<your file here.csv>)
  .pipe(csv())
  .on('data', (data) => resp.push(data))
  .on('end', () => {
    let coords = [];

    const stepList = [];

    Array.from(googleResponse.routes[0].legs).forEach(leg => {
      stepList.push(...leg.steps);
    });

    stepList.forEach(e => {
      coords.push([e.endLocation.lat, e.endLocation.lng]);
    });

    coords.push([legList[0].startLocation.lat, legList[0].startLocation.lng]);

    let arr = [];
    let promises = [];
    let bboxes = [];

    coords.forEach(coord => {
      let results = search(coords);

      let thisPlace = results.address.city || results.address.town || results.address.village;
      thisPlace && arr.indexOf(thisPlace) === -1 && arr.push(thisPlace);
    };

    /*The rest of your code*/
});

The next thing we need is the actual search function, which is the complicated bit. We need to find something that's quick, but also mostly correct. The actual implementation depends on the format of your file, but here's a quick rundown of what I'd do:

  • Create two duplicates of resp, but sort one (we'll call this array a_o) by longitude and the other one by latitude (a_a). Ensure you don't use var or let or const when defining these arrays, just... define them.
  • For each one, remove anything not within a 25km (radius) of the point on the longitude axis in a_o, and the same but with latitude for a_a
  • delete both arrays to clear the space they are taking up in the RAM.
  • Find any item that's in both arrays, and put these in an array called a_c
  • Filter any items which are within 3-4km of each other (make sure to keep one of the points, though, not delete both!)
  • Go through each item, and work out the absolute distance to the point (using this algorithm - remember, the earth is a sphere, basic Pythagorean thereom will not work!
  • If you find any item with a distance less than 20km, and has a city or village or town attached, break and return the name.
  • If you finish, i.e never break, return undefined.

Other then that, you can go with mostly any CSV which contains:

  • The city's name
  • The central latitude
  • The central longitude

I hope this helps. In conclusion, go with the first, tried-and-tested, premade method if you're only doing 5-6 simple routes an hour. If you've got gajillions of waypoints, download the data and spend half an hour or so making what is essentially your own geocoding system. It'll well be worth the performance bump.

Geza Kerecsenyi
  • 1,127
  • 10
  • 27
  • Thank you for the detailed answer. I'll definitely go for the second option, since the amount of requests is closer to _gajilions_. :) – Eric Wu Aug 12 '19 at 16:50
  • @EricWu If it's not too late, see now: I noticed a mistake with the second code example, where I unnecessarily used recursion. This was only done to make a more compactified version when doing requests to the API, but it's not necessary if you're just pushing to an array. Plus, if you are really making *that* many requests, it would cause a `Maximum stack call size exceeded error` - I reasonably would assume, however, that anyone with few enough requests to justify using the API would also not have a problem with some recursion. – Geza Kerecsenyi Aug 12 '19 at 19:21
  • Not late at all :). And yes, recursion is not an issue. – Eric Wu Aug 12 '19 at 19:28