はじめに
今回は OpenLayers と OpenStreetMap を使ってkintoneに登録した写真の位置情報を地図に表示するサンプルを紹介します。
OpenStreetMapとは「道路地図などの地理情報データを誰でも利用できるよう、フリーの地理情報データを作成することを目的としたプロジェクト」です。
無償で地図を利用できることが大きなメリットになります。
OpenStreetMapのライセンスについては
ライセンスとプライバシーポリシーについて
を確認してください。
注意事項
- モバイルなど端末の位置情報が無効の場合や、縮小版の写真を登録すると位置情報が取得できません。
- kintoneモバイルもしくはブラウザーから「写真またはビデオを撮る」メニューを使って、直接撮影した写真には位置情報が含まれないことを確認しています。
デモ環境
デモ環境
で実際に動作を確認できます。
ログイン情報は
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
でいろいろ試していただければと思います。