Geolocation API(位置情報)を使ったモバイル用のカスタマイズをしよう

著者名: 山下 竜 (External link) (kintoneエバンジェリスト)

目次

caution
警告

2020年8月改訂のセキュアコーディング ガイドライン に抵触する内容が含まれています。
認証情報が漏洩した場合の影響を考慮して慎重に検討してください。
該当箇所:JavaScript プログラムの 10 行目

はじめに

kintone ではモバイル用の JavaScript も収容できるようになり、関連する情報も充実してきました。
これでモバイルでの用途や使いやすさはさらに増していくことと思います。

今回は、Geolocation API を使ったシンプルな kintone モバイルアプリを作ってみたいと思います。
Geolocation API は HTML5 の標準 API の 1 つです。 kintone に保存するデータとして場所と時間を一緒に押さえておくことで、便利に使えることがあります。
現場の懸案管理、訪問管理など屋外の動向管理に役に立つと思いますので、お試しいただければと思います。

やや長めですが、お付き合いいただければ幸いです。

tips
補足

サンプルでは、地図の常時に Google Maps Platform の Maps JavaScript API を使用しています。
ご利用方法によっては有償ライセンスの購入が必要です。 Google のライセンスを確認してください。

準備とJavaScriptの適用方法

次のフィールドを含むアプリをご準備ください。

フィールドタイプ フィールド名(例) フィールドコード
文字列(1行) 緯度 lat
文字列(1行) 経度 lng
スペース Map
レコード番号 レコード番号 record_no

JavaScript を次の手順で適用します。

  1. JavaScript ソースコードを sample.js 等ファイル名を付けて保存する。
  2. 「PC 用の JavaScript ファイル」と「モバイル用の JavaScript ファイル」にそれぞれ同じファイルをアップする。

ソースコードは JavaScriptソースコード を参照してください。

完成イメージ(モバイル画面)

見た目は「 レコード一覧で顧客訪問リストを地図にピン表示する」と似たような感じになります。
違うのは緯度経度の取得方法で、登録済みレコードの詳細画面で「緯度経度更新」ボタンを押すことで、緯度経度の値を更新することがきます。

JavaScriptソースコード

10 行目の YOUR_GOOGLE_MAPS_JAVASCRIPT_API_KEY の部分を取得した Maps JavaScript API キーに書き換えてください。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/**
 * Sample program
 * Copyright (c) 2014 Cybozu
 *
 * Licensed under the MIT License
 */
(function() {
  'use strict';

  const api_key = 'YOUR_GOOGLE_MAPS_JAVASCRIPT_API_KEY';

  // ヘッダに要素を追加
  function load(src) {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    let head;
    // PC用の場合(一覧画面)
    if (kintone.app.getHeaderSpaceElement()) {
      try {
        head = kintone.app.getHeaderSpaceElement();
        head.appendChild(script);
      } catch (e) {
        return;
      }
    }
    // PC用の場合(詳細画面)
    if (kintone.app.record.getHeaderMenuSpaceElement()) {
      try {
        head = kintone.app.record.getHeaderMenuSpaceElement();
        head.appendChild(script);
      } catch (e) {
        return;
      }
    }
    // モバイル用の場合
    if (kintone.mobile.app.getHeaderSpaceElement()) {
      try {
        head = kintone.mobile.app.getHeaderSpaceElement();
        head.appendChild(script);
      } catch (e) {
        return;
      }
    }
  }

  // 緯度、経度を空にする
  function emptyLatLng(event) {
    const rec = event.record;
    // 保存の際に緯度、経度を空にする
    rec.lat.value = '';
    rec.lng.value = '';
    return event;
  }

  // 詳細画面を開いた時に実行します
  function detailShow(event) {
    loadGMap();
    waitLoaded(event, 'detail', 10000, 100);
  }

  // 一覧画面を開いた時に実行します
  function indexShow(event) {
    loadGMap();
    waitLoaded(event, 'index', 10000, 100);
  }

  // Google Maps APIをロード
  function loadGMap() {
    // document.write を定義
    const nativeWrite = document.write;
    document.write = function(html) {
      const m = html.match(/script.+src="([^"]+)"/);
      if (m) {
        load(m[1]);
      } else {
        nativeWrite(html);
      }
    };

    // Google Map の API ライブラリをロード
    load('https://maps.googleapis.com/maps/api/js?v=3&key=' + api_key);
  }

  // Google Maps APIがロードされるまで待機
  function waitLoaded(event, mode, timeout, interval) {
    setTimeout(() => {
      timeout -= interval;
      if (
        typeof google !== 'undefined' &&
        typeof google.maps !== 'undefined' &&
        typeof google.maps.version !== 'undefined'
      ) {
        if (mode === 'detail') {
          // 詳細画面の場合
          setLocationDetail(event);
        } else if (mode === 'index') {
          // 一覧画面の場合
          setLocationIndex(event);
        }
      } else if (timeout > 0) {
        // ロードされるまで繰り返す
        waitLoaded(event, mode, timeout, interval);
      }
    }, interval);
  }

  function setLocationIndex(event) {
    const lat = [];
    const lng = [];
    const record_no = [];

    // レコード番号と緯度経度を取得
    const rec = event.records;

    for (let i = 0; i < rec.length; i++) {
      lat.push(parseFloat(rec[i].lat.value));
      lng.push(parseFloat(rec[i].lng.value));
      record_no.push(parseInt(rec[i].record_no.value, 10));
    }

    // ポイントを先に作成
    let latlng = 0;

    for (let y = 0; y < lat.length; y += 1) {
      if (isNaN(lat[y]) === false && isNaN(lng[y]) === false) {
        latlng = new google.maps.LatLng(lat[y], lng[y]);
        break;
      }
    }

    // 緯度・経度に値が入ったレコードがなければ、ここで終了
    if (latlng === 0) {
      return;
    }

    // 地図の要素を作成
    const elMap = document.createElement('div');
    elMap.setAttribute('id', 'map');
    elMap.setAttribute('name', 'map');
    // コンテナ要素を定義
    let elMapContainer;
    if (event.type === 'app.record.index.show') {
      // PC用
      elMap.setAttribute(
        'style',
        'width: auto; height: 250px; margin-right: 30px; border: solid 2px #c4b097'
      );
      elMapContainer = kintone.app.getHeaderSpaceElement();
    } else if (event.type === 'mobile.app.record.index.show') {
      // スマホ用
      elMap.setAttribute('style', 'width: auto; height: 150px;');
      elMapContainer = kintone.mobile.app.getHeaderSpaceElement();
    }

    // イベントの多重化による要素生成に対応
    const check = document.getElementsByName('map');
    if (check.length !== 0) {
      elMapContainer.removeChild(check[0]);
    }

    // コンテナ要素に地図要素を追加
    elMapContainer.insertBefore(elMap, elMapContainer.firstChild);

    // Google Mapに表示する地図の設定を行います
    const opts = {
      zoom: 12,
      center: latlng,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      scaleControl: true,
      title: 'target'
    };

    // 地図の要素を定義
    const map = new google.maps.Map(document.getElementById('map'), opts);

    // マーカーの設定
    const marker = [];
    const m_latlng = [];

    for (let n = 0; n < lat.length; n += 1) {
      if (isNaN(lat[n]) === false && isNaN(lng[n]) === false) {
        m_latlng[n] = new google.maps.LatLng(lat[n], lng[n]);
        marker[n] = new google.maps.Marker({
          position: m_latlng[n],
          map: map,
          icon:
            'https://chart.googleapis.com/chart?chst=d_bubble_text_small&chld=edge_bc|' +
            record_no[n] +
            '|FF8060|000000'
        });
      }
    }
    return event;
  }

  function setLocationDetail(event) {
    const rec = event.record;
    let elMapContainer;

    // レコード登録直後等、緯度経度が空の場合は終了
    if (rec.lat.value === 0 || rec.lng.value === 0) {
      return;
    }

    // 地図を配置するための要素を取得する
    if (event.type === 'app.record.detail.show') {
      // PC用
      elMapContainer = kintone.app.record.getSpaceElement('Map');
      if (elMapContainer === undefined) {
        // 「Map」のスペース要素が無い場合は終了
        return;
      }
      // 親要素のサイズを強制変更
      const elMapContainerParent = elMapContainer.parentNode;
      elMapContainerParent.setAttribute('style', 'width: 300px; height: 250px');
    } else if (event.type === 'mobile.app.record.detail.show') {
      // スマホ用
      elMapContainer = kintone.mobile.app.getHeaderSpaceElement();
    }

    // イベントの多重化による要素生成に対応
    const check = document.getElementsByName('map');
    if (check.length !== 0) {
      elMapContainer.removeChild(check[0]);
    }

    // 地図用の要素を作成
    const elMap = document.createElement('div');
    elMap.setAttribute('id', 'map');
    elMap.setAttribute('name', 'map');

    // コンテナ要素に地図要素を追加
    elMapContainer.insertBefore(elMap, elMapContainer.firstChild);

    elMap.setAttribute('style', 'width: auto; height: 250px');

    // Google Mapの設定
    // ポイントする座標を指定
    const point = new google.maps.LatLng(rec.lat.value, rec.lng.value);

    // 地図の表示の設定(中心の位置、ズームサイズ等)を設定
    const opts = {
      zoom: 15,
      center: point,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      scaleControl: true
    };

    // 地図を表示する要素を呼び出し
    const map = new google.maps.Map(document.getElementById('map'), opts);

    // マーカーの設定
    const marker = new google.maps.Marker({
      position: point,
      map: map,
      title: ''
    });
    return event;
  }

  // 緯度経度を取得し、当該レコードを更新
  function getPosition(event) {
    navigator.geolocation.getCurrentPosition((position) => {
      let appId, recordId;
      if (event.type === 'app.record.detail.show') {
        // PC用
        appId = kintone.app.getId();
        recordId = kintone.app.record.getId();
      } else if (event.type === 'mobile.app.record.detail.show') {
        // スマホ用
        appId = kintone.mobile.app.getId();
        recordId = kintone.mobile.app.record.getId();
      }
      const objParam = {
        app: appId,
        id: recordId,
        record: {
          lat: {
            value: position.coords.latitude
          },
          lng: {
            value: position.coords.longitude
          }
        }
      };
      // レコードを更新
      kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', objParam, () => {
        // 成功時は画面をリロード
        location.reload(true);
      }, (resp) => {
        // エラー時はメッセージを表示して、処理を中断
        alert('error->' + resp);

      });
    }, errorCallback);
  }

  // 緯度経度取得失敗時コールバック関数
  function errorCallback(error) {
    let errMsg = '';
    switch (error.code) {
      case 1:
        errMsg = '位置情報の利用が許可されていません';
        break;
      case 2:
        errMsg = 'デバイスの位置が判定できません';
        break;
      case 3:
        errMsg = 'タイムアウトしました';
        break;
    }
    alert(errMsg);
  }

  // 緯度経度を再取得するための(ボタンを配置する)関数
  function updatePosition(event) {
    const check = document.getElementsByName('update');
    if (check.length === 0) {
      const button = document.createElement('button');
      button.appendChild(document.createTextNode('緯度経度更新'));
      button.setAttribute('name', 'update');

      const span = document.createElement('span');
      span.appendChild(button);

      // ボタンを配置するスペースを取得
      let elButtonSpace;
      if (event.type === 'app.record.detail.show') {
        // PC用
        elButtonSpace = kintone.app.record.getHeaderMenuSpaceElement();
      } else if (event.type === 'mobile.app.record.detail.show') {
        // スマホ用
        elButtonSpace = kintone.mobile.app.getHeaderSpaceElement();
      }
      elButtonSpace.appendChild(span);

      button.addEventListener('click', () => {
        getPosition(event);
      });
    }
  }

  // 各種イベントハンドル
  kintone.events.on(['app.record.create.submit',
    'app.record.edit.submit',
    'app.record.index.edit.submit', 'mobile.app.record.create.submit',
    'mobile.app.record.edit.submit', 'mobile.app.record.index.edit.submit'], emptyLatLng);

  kintone.events.on(['app.record.index.show', 'mobile.app.record.index.show'], indexShow);
  kintone.events.on(['app.record.detail.show', 'mobile.app.record.detail.show'], detailShow);
  kintone.events.on(['app.record.detail.show', 'mobile.app.record.detail.show'], updatePosition);

})();

ソースコードの説明

「レコード一覧で顧客訪問リストを地図にピン表示する」との違いは次の 3 ポイントですので、このあたりを中心に説明します。

  • 緯度経度を Geolocation API で取得
  • モバイルに対応
  • PC 用とモバイル用の JavaScript を 1 つのファイルに記述(イベント以外の関数の共用)

一覧画面で地図を配置する要素の設定

やはり PC 用とスマホ用で違ってきます。
スマホ用ではスクロールに耐えられるよう高さを 150px としました。
また、PC 用とスマホ用でヘッダースペース要素の取得関数が異なるため、これらを使い分けます。

  • PC 用:kintone.app.getHeaderSpaceElement
  • スマホ用:kintone.mobile.app.getHeaderSpaceElement
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if (event.type === 'app.record.index.show') {
  // PC用
  elMap.setAttribute(
    'style',
    'width: auto; height: 250px; margin-right: 30px; border: solid 2px #c4b097'
  );
  elMapContainer = kintone.app.getHeaderSpaceElement();
} else if (event.type === 'mobile.app.record.index.show') {
  // スマホ用
  elMap.setAttribute('style', 'width: auto; height: 150px;');
  elMapContainer = kintone.mobile.app.getHeaderSpaceElement();
}

詳細画面における地図を配置する要素の設定

ここも PC 用とスマホ用で違ってきます。
PC 用では要素 ID が「Map」のスペースフィールドに、スマホ用では kintone.mobile.app.getHeaderSpaceElement を使って取得する要素に配置します。
合わせて、PC 用では、親要素のサイズを変更しておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 地図を配置するための要素を取得する
if (event.type === 'app.record.detail.show') {
  // PC用
  elMapContainer = kintone.app.record.getSpaceElement('Map');
  if (elMapContainer === undefined) {
    // 「Map」のスペース要素が無い場合は終了
    return;
  }
  // 親要素のサイズを強制変更
  const elMapContainerParent = elMapContainer.parentNode;
  elMapContainerParent.setAttribute('style', 'width: 300px; height: 250px');
} else if (event.type === 'mobile.app.record.detail.show') {
  // スマホ用
  elMapContainer = kintone.mobile.app.getHeaderSpaceElement();
}

緯度経度を取得し、レコードを更新する関数

Geolocation API で取得した値を用いてレコード(緯度、経度)を更新します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 緯度経度を取得し、当該レコードを更新
function getPosition(event) {
  navigator.geolocation.getCurrentPosition((position) => {
    let appId, recordId;
    if (event.type === 'app.record.detail.show') {
      // PC用
      appId = kintone.app.getId();
      recordId = kintone.app.record.getId();
    } else if (event.type === 'mobile.app.record.detail.show') {
      // スマホ用
      appId = kintone.mobile.app.getId();
      recordId = kintone.mobile.app.record.getId();
    }
    const objParam = {
      app: appId,
      id: recordId,
      record: {
        lat: {
          value: position.coords.latitude
        },
        lng: {
          value: position.coords.longitude
        }
      }
    };
      // レコードを更新
    kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', objParam, () => {
      // 成功時は画面をリロード
      location.reload(true);
    }, (resp) => {
      // エラー時はメッセージを表示して、処理を中断
      alert('error->' + resp);

    });
  }, errorCallback);
}

注意事項

今回の注意点は次のとおりです。

  • 筆者は Macintosh の Google Chrome と iOS 7 の Safari で動作確認を行っています。
  • 「addEventListener」はブラウザーによって対応が異なるので、それぞれ確認してください。
  • 今回は地図ライブラリとして「Google Maps API」を用いていますが、ライセンス等利用に関する制限は各自で確認してください。

最後に

やや長めの Tip となってしまいましたが、いかがでしたでしょうか?
今回は Geolocation API を利用することで、タブレットやスマホからのレコード登録に合わせて位置情報を取り込む例を紹介しました。

クラウドはモバイルとの親和性を高めることでより便利に使うことができますが、現状ではスマホ用のサンプルも少ないので、皆さまの参考になれば幸いです。

変更履歴

  • 2019/04/05
    • Maps JavaScript API キーを使用したコードに修正
  • 2019/06/07
    • PC 版とモバイル版での処理を共通化(モバイル版のレコード一覧表示イベントで event.records が取得可能になったため)
information

この Tips は、2019 年 5 月版 kintone で動作を確認しています。