OpenStreetMap で写真の位置情報を表示する

著者名:サイボウズ 竹内 能彦

目次

はじめに

今回は OpenLayers (External link) OpenStreetMap (External link) を使って kintone に登録した写真の位置情報を地図に表示するサンプルを紹介します。

OpenStreetMap とは「道路地図などの地理情報データを誰でも利用できるよう、フリーの地理情報データを作成することを目的としたプロジェクト」です。
無償で地図を利用できることが大きなメリットになります。
OpenStreetMap のライセンスについては ライセンスとプライバシーポリシーについて (External link) を確認してください。

注意事項

  • モバイルなど端末の位置情報が無効の場合や、縮小版の写真を登録すると位置情報が取得できません。
  • kintone モバイルもしくはブラウザーから「写真またはビデオを撮る」メニューを使って、直接撮影した写真には位置情報が含まれないことを確認しています。

デモ環境

デモ環境 (External link) で実際に動作を確認できます。
ログイン情報は cybozu developer network デモ環境 で確認してください。

結果

まずは結果からご覧いただきましょう。

画像が一枚の場合

レコードに画像を一枚しか登録しない場合は撮影地点を中心に地図を表示します。

画像が複数枚の場合

複数枚登録した場合は、すべてのピンを地図内に収めるようにズームを自動調整します。

位置情報が取得できない場合

位置情報が取得できない場合はメッセージを表示します。

一覧画面での見え方

一覧画面にも地図を表示します。

アプリの準備

フィールドの設定(今回のカスタマイズで必要なフィールドのみを抜粋)

フィールド名 フィールドタイプ フィールドコード
写真 添付ファイル pic
スペース map

JS / CSS 設定

「アプリの設定 > JavaScript / CSS でカスタマイズ」の「PC 用の CSS ファイル」に以下の URL を設定します。

  • https://js.cybozu.com/openlayers/v3.17.1/ol.css

「アプリの設定 > JavaScript / CSS でカスタマイズ」の「PC 用の JavaScript ファイル」に以下の URL / ファイルを設定します。

  • https://js.cybozu.com/jquery/2.2.4/jquery.min.js

  • https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.1.0/load-image.all.min.js

  • https://js.cybozu.com/openlayers/v3.17.1/ol.js

  • sample.js
    以下のサンプルコードをエディタにコピーして、ファイル名を「sample.js」、文字コードを「UTF-8N」で保存し、アップロードします。
    ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

      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
    
    /*
     * kintone x OpenStreetMap
     * Copyright (c) 2016 Cybozu
     *
     * Licensed under the MIT License
     * https://opensource.org/license/mit/
     */
    (function() {
      'use strict';
    
      // kintoneに添付されたファイルをダウンロード
      function getFile(url) {
        const df = new $.Deferred();
        const xhr = new XMLHttpRequest();
    
        xhr.open('GET', url, true);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.responseType = 'blob';
    
        xhr.onload = function(e) {
          if (this.status === 200) {
            df.resolve(this.response);
          }
        };
    
        xhr.send();
        return df.promise();
      }
    
      // 度分秒から百分率に変換
      function toPercentage(ref, geo) {
        if (ref === 'N' || ref === 'E') {
          return geo[0] + geo[1] / 60 + geo[2] / 3600;
        } else if (ref === 'S' || ref === 'W') {
          return -(geo[0] + geo[1] / 60 + geo[2] / 3600);
        }
      }
    
      // EXIFの座標情報を取得
      function getExif(imageData) {
        const df = new $.Deferred();
        loadImage.parseMetaData(imageData, (data) => {
          // EXIFデータがない場合
          if (data.exif === undefined) {
            return df.resolve();
          }
    
          const gpsLatitude = data.exif.get('GPSLatitude');
          const gpsLatitudeRef = data.exif.get('GPSLatitudeRef');
          const gpsLongitude = data.exif.get('GPSLongitude');
          const gpsLongitudeRef = data.exif.get('GPSLongitudeRef');
    
          const latitude = toPercentage(gpsLatitudeRef, gpsLatitude);
          const longitude = toPercentage(gpsLongitudeRef, gpsLongitude);
    
          const position = {longitude: longitude, latitude: latitude};
    
          df.resolve(position);
        });
        return df.promise();
      }
    
      // 緯度経度を球面メルカトル図法に変換
      function convertCoordinate(longitude, latitude) {
        return ol.proj.transform([longitude, latitude], 'EPSG:4326', 'EPSG:3857');
      }
    
      // マーカーを表示するレイヤーを作成
      function makeMarkerOverlay(coordinate) {
        const imgElement = document.createElement('img');
        const imgSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAZCAYAAADe1WXtAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHwSURBVEiJpdW9a9NRFMbxT9qmNWmSRqlFhE6CLuIL1DcUHDqJm/0HBEUk3RQXpW7dXdysWlAEHQSlo4IoIohQKFJQxKWIIlTb5pc2anMdkmBbmleHZ3vul3Pvc865QggaCfu6uIOtTfkbGboYTRIN8yvJd5xoG4psmqk9LH0gBMJTQh+FJOPobAmKo718zbFcrACr+kI4TpThHQYbQhHr4WqG6MkG2FqtEsb5nWQJZ2pCMZDh5RBLc3WAa/WGsIMozV0k1kExnGR+jOKfJoFVLRBGKKT4jL0hBLq51kfhRZ2D04Qblfes5blNKcFynItwYTf5Ug3zc0KyolSlslrgIQpxxiCWYWaC0mbGUUIHASFDeFwD+Kjcbh/RWX3Tw1mixTYrzRP6iXBsXfppHl6m2M6bXqLYx4PNWmpnguhTi+m/L99iEf2bNn8P108RtQI9Qj5Ort5Ebenl27MmgfcoZZhFR93Zx8gu8o2GYIGQLYdzqKmFkuHtTVbrQXOsZJhsZUvtT1P4UQM4/S+cbS3t0xSTOVY2AkuEA+Q7Od/Okt6eID+7ATpRDmcGsba+k26unCRfBc6XRzXCwba+k0q18RRzUxXoOZZT3Kp3piG0Aj49SP41IcFPZP8bWmmxVwOsxDjbjL8pKPZ3c79eOGv1F5xHWAKxXNwiAAAAAElFTkSuQmCC';
        imgElement.setAttribute('src', imgSrc);
    
        const markerOverlay = new ol.Overlay({
          element: imgElement,
          position: coordinate,
          positioning: 'center-center'
        });
    
        return markerOverlay;
      }
    
      // 地図を表示し、ピンを立てる
      function setPin(space, fileKeyList) {
        const map = new ol.Map({
          target: 'map',
          layers: [
            new ol.layer.Tile({
              source: new ol.source.OSM()
            })
          ],
          view: new ol.View({
            zoom: 15
          })
        });
    
        Promise.all(fileKeyList.map((fileKey) => {
          const fileUrl = '/k/v1/file.json?fileKey=' + fileKey;
          return getFile(fileUrl);
        })).then((imageBlobList) => {
          return Promise.all(imageBlobList.map((imageBlob) => {
            return getExif(imageBlob);
          }));
        }).then((positionList) => {
          let existPosition = false;
    
          let minLongitude = 180,
            minLatitude = 90;
          let maxLongitude = -180,
            maxLatitude = -90;
          for (let i = 0; i < positionList.length; i++) {
            const position = positionList[i];
            // EXIFデータがない場合
            if (position === undefined || position.longitude === undefined || position.latitude === undefined) {
              continue;
            }
            existPosition = true;
    
            const longitude = position.longitude;
            const latitude = position.latitude;
            const coordinate = convertCoordinate(longitude, latitude);
            const marker = makeMarkerOverlay(coordinate);
            map.addOverlay(marker);
    
            if (longitude < minLongitude) {
              minLongitude = longitude;
            }
            if (latitude < minLatitude) {
              minLatitude = latitude;
            }
            if (longitude > maxLongitude) {
              maxLongitude = longitude;
            }
            if (latitude > maxLatitude) {
              maxLatitude = latitude;
            }
          }
    
          if (existPosition === false) {
            $(space).text('位置情報が取得できないため、地図を表示できません');
            $(space).css('text-align', 'center').css('padding', '20px');
          } else if ((minLongitude === maxLongitude) && (minLatitude === maxLatitude)) {
            map.getView().setCenter(convertCoordinate(minLongitude, minLatitude));
          } else {
            // 座標が複数の場合は、中心を計算する
            const extent = ol.proj.transformExtent([minLongitude, minLatitude, maxLongitude, maxLatitude],
              'EPSG:4326', 'EPSG:3857');
            map.getView().fit(extent, map.getSize());
          }
        }).catch((error) => {
          console.log('ERROR', error);
        });
      }
    
      kintone.events.on('app.record.detail.show', (event) => {
        const record = event.record;
    
        const space = kintone.app.record.getSpaceElement('map');
        $(space).append('<div id="map" style="width:400px; height:400px"></div>');
    
        const fileKeyList = [];
        for (let i = 0; i < record.pic.value.length; i++) {
          const fileKey = record.pic.value[i].fileKey;
          fileKeyList.push(fileKey);
        }
    
        setPin(space, fileKeyList);
      });
    
      kintone.events.on('app.record.index.show', (event) => {
        // 地図を表示済みの場合は一旦削除
        if ($('div#map').length > 0) {
          $('div#map').remove();
        }
    
        const space = kintone.app.getHeaderSpaceElement();
        $(space).append('<div id="map" style="width:90%; height:400px"></div>');
        $('div#map').css('margin', '5px auto');
    
        const fileKeyList = [];
        for (let i = 0; i < event.records.length; i++) {
          const record = event.records[i];
          for (let j = 0; j < record.pic.value.length; j++) {
            const fileKey = record.pic.value[j].fileKey;
            fileKeyList.push(fileKey);
          }
        }
    
        setPin(space, fileKeyList);
      });
    
    })();

おわりに

地図の表示に利用した OpenLayers にはさまざまな機能があります。
詳細は、 OpenLayers Examples (External link) でいろいろ試していただければと思います。