GAS で Google Maps API を利用して目的地までの所要時間を取得する

- ある地点から 目的地までの所要時間を Google Apps Script でまとめて取得しました

## TL;DR

- `Maps.newDirectionFinder()` で Google Maps Directions API を利用した経路検索ができます
  - origin (出発地), destination (目的地), mode (移動手段) をパラメータに指定します
- 所要時間は `directions.routes[0].legs[0].duration` で取得できます

```js
const origin = 'XXXXX' // 出発地の名称を記入してください

const directions = Maps.newDirectionFinder()
  .setOrigin(origin)
  .setDestination(dest)
  .setMode(Maps.DirectionFinder.Mode.DRIVING)
  .getDirections()

const duration = directions.routes[0].legs[0].duration
```

## 目次

- はじめに
- 大まかな流れ
- スクリプト実行前のスプレッドシート (例)
- 実装
  - スプレッドシートから値を取得する
  - 目的地までの所要時間を取得する
  - スプレッドシートに 所要時間 (テキスト, 数値) を記入する
- 全体のコード
- スクリプト実行後のスプレッドシート (例)
- おわりに
- 補足
- 参考

## はじめに

- 今回は Standalone ではなく Container-bound Script として GAS を作成します
  - 紐付くスプレッドシートに対する 読み/書き を行います
- 読み/書き の権限を与えるため `appsscript.json` (manifest file) に以下を追記します

```json
{
  ...
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.currentonly"
  ],
  ...
}
```

## 大まかな流れ

- スプレッドシートにあらかじめ 目的地の名称を記入しておきます
- GAS を実行して 目的地までのそれぞれの所要時間 を スプレッドシートに記入します
- スプレッドシート側で 所要時間の少ない順にソートできるようにします

## スクリプト実行前のスプレッドシート (例)

destination | duration.text | duration.value
--- | --- | ---
ららぽーと富士見 |   |  
コクーンシティ |   |  
浦和PARCO |   |  
ららぽーと新三郷 |   |  
イオンレイクタウン |   |  

## 実装

### スプレッドシートから値を取得する

- `getDataRange()` でデータが存在する範囲を取得します

```js
// slice(1) to skip the header
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1)

// row[0] is the column of destinations
return rows.map(function (row) { return row[0] })
```

### 目的地までの所要時間を取得する

- `Maps.newDirectionFinder()` を利用します
- 今回は 車での経路取得をしました

```js
const directions = Maps.newDirectionFinder()
  .setOrigin(origin)
  .setDestination(dest)
  .setMode(Maps.DirectionFinder.Mode.DRIVING)
  .getDirections()
```

- のちにソートするために 所要時間の秒数表記の値 `duration.value` も取得しておきます

```json
"duration" : {
  "text" : "51 mins",
  "value" : 3062
}
```

- のちにスプレッドシートに書き込むために object を 配列に変換しておきます

```js
const duration = directions.routes[0].legs[0].duration

// Object.values() doesn't exist in Google Apps Script
return Object.keys(duration).map(function (key) { return duration[key] })
```

### スプレッドシートに 所要時間 (テキスト, 数値) を記入する

- `getRange(row, column, numRows, numColumns)` で durations を書き込むシートの範囲を取得します
- `range.setValues` で二次元配列の `durations` をシートに記入します

```js
// slice(1) to skip the header
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1)

// NOTE: getRange(row, column, numRows, numColumns)
const range = SpreadsheetApp.getActiveSheet().getRange(2, 2, rows.length, 2)

range.setValues(durations)
```

## 全体のコード

```js
// Goal:
//   Find the closest store
//
// Usage:
//   Run the code on the Script Editor manually
//
// Summary:
//   Return duration.text and duration.value to the spreadsheet
//   Sort by duration.value on the spreadsheet
//
// Description:
//   1. Obtain destinations from the spreadsheet
//   2. Call calcDuration(dest) for each destinations
//   3. Calculate a duration
//   4. Update the duration.text and duration.value on the spreadsheet

function getDuration() {
  const destinations = readDestinations()

  const durations = destinations.map(function (dest) {
    return calcDuration(dest)
  })

  writeDurations(durations)
}

function readDestinations() {
  const rows = getTargetedRows()

  // row[0] is the column of destinations
  return rows.map(function (row) { return row[0] })
}

function writeDurations(durations) {
  const rows = getTargetedRows()

  // NOTE: getRange(row, column, numRows, numColumns)
  const range = SpreadsheetApp.getActiveSheet().getRange(2, 2, rows.length, 2)
  
  range.setValues(durations)
}

// Return duration.text and duration.value in a multi-dimensional array
function calcDuration(dest) {
  // Hard-coding the value of `origin`
  const origin = 'XXXXX' // 出発地の名称を記入してください

  const directions = Maps.newDirectionFinder()
    .setOrigin(origin)
    .setDestination(dest)
    .setMode(Maps.DirectionFinder.Mode.DRIVING)
    .getDirections()

  const duration = directions.routes[0].legs[0].duration

  // Object.values() doesn't exist in Google Apps Script
  return Object.keys(duration).map(function (key) { return duration[key] })
}

function getTargetedRows() {
  // slice(1) to skip the header
  return SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1)
}
```

## スクリプト実行後 スプレッドシート (例)

destination | duration.text | duration.value
--- | --- | ---
ららぽーと富士見 | 35 mins | 2102
コクーンシティ | 51 mins | 3039
浦和PARCO | 52 mins | 3094
ららぽーと新三郷 | 1 hour 2 mins | 3708
イオンレイクタウン | 1 hour 5 mins | 3875

## おわりに

- 私は埼玉に土地勘がなく 商業施設の名称を見ても出発地からの距離感がわからなかったのですが GAS を使うことで 近くの店舗を見つけることができました
- まだ試したことはないのですが 次回は Google Charts を利用して Map にプロットできると良さそうですね

## 補足

- スプレッドシートの 読み/書き に `getDataRange()` を利用したのですが あまりスマートなやり方とは思えません...
- より良い方法をご教授いただけると幸いです :bow:

> スプレッドシートから目的地の名称を取得

```js
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1)

// column A
return rows.map(function (row) { return row[0] })
```

> 所要時間を記入するシートの範囲を取得する

```js
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1)

// column B, C
const range = SpreadsheetApp.getActiveSheet().getRange(2, 2, rows.length, 2)
```

## 参考

- [Container\-bound Scripts  \|  Apps Script  \|  Google Developers](https://developers.google.com/apps-script/guides/bound#special_methods)
- [Class DirectionFinder  \|  Apps Script  \|  Google Developers](https://developers.google.com/apps-script/reference/maps/direction-finder#getDirections())
- [Class SpreadsheetApp  \|  Apps Script  \|  Google Developers](https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app#authorization)
- [Authorization Scopes  \|  Apps Script  \|  Google Developers](https://developers.google.com/apps-script/concepts/scopes#setting_explicit_scopes)
- [Reading & Writing Cell Values  \|  Sheets API  \|  Google Developers](https://developers.google.com/sheets/api/guides/values)