Google Apps Scriptでマップ作成とKML生成【GAS】

現在、Google Spreadsheetへ住所やキーワード、ランドマーク名などを入力して、そのシートを元にマップを生成するツールを作って実際に使っていたりします。以前作っていた、GE Maniacsというサイトでは、Google Earthをネタにサイト運営をしてた関係で、こちらのサイトのツールなども利用させていただいてました。この住所やキーワードを元にマップを作る際にやっておくべき事がジオコーディングという作業で、住所等を「緯度経度」に変換しておく作業です。

実際にはマップにレンダリングする際には、緯度経度情報じゃなく住所でもそのまま投げて、サーバー側で緯度経度に変換してくれるので、問題はないのですが、都度変換されることになるので効率が悪く遅くなります。事前に緯度経度に変えておけば高速にマッピングが出来るわけです。ちなみに、メソッドにはリバースジオコーディング(緯度経度から住所を得る)のメソッドも合ったりします。

※ちなみに自分のツールは、スプレッドシート上でマップを表示させてみたり、Static Mapの画像を取得してみたりなどやってます。

今回使用するファイルとアプリケーション

※今回使用するスプレッドシートにジオコーディングキーワードに住所を入れてメニューよりジオコーディングを実施すると一括でスプレッドシートの住所一覧から緯度経度情報を返してくれます。C列~E列を消して実行するとわかります。

ジオコーディングソース

GAS側コード

function allgeocode(flag){
  //シートデータ取得準備
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("mapsheet");
  var range = sheet.getRange("A2:F").getValues();
  var num = range.length;
  var finalrow = sheet.getLastRow()-1;
  
  //flag値が1の時、サイレントモード
  if(flag == 1){
  
  }else{
    var ui = SpreadsheetApp.getUi();
    var re = ui.alert("緯度経度変換", "住所等データから緯度経度変換しますか?", ui.ButtonSet.YES_NO_CANCEL);
    switch(re){
      case ui.Button.YES:
        //プログレスダイアログを表示
        var html = HtmlService.createHtmlOutputFromFile('dialog').setSandboxMode(HtmlService.SandboxMode.IFRAME);
        ui.showModalDialog(html, '変換中・・・');
        break;
      case ui.Button.NO:
        ui.alert("変換を中止しました。");
        return 0;
        break;
      case ui.Button.CANCEL:
        ui.alert("変換をキャンセルしました。");
        return 0;
       break;
    }   
  }
  
  
  //緯度経度情報格納用二次元配列を作成
  //二次元配列を作成する(4列・行数分+1)
  var msgcnt =3;
  var dataArray = new Array(finalrow); 
  for(i = 0; i < finalrow; i++){
    dataArray[i] = new Array(msgcnt); 
  }
  
  //住所データ(U列)よりジオコーディングをし、結果をV列に書き込む
  for(var i = 0;i<finalrow;i++){ 
    //住所を取得
    var address = range[i][1];
    
    //住所が空の場合はスルーする
    if(address == ""){
      dataArray[i][0] = "";
      dataArray[i][1] = "";
      dataArray[i][2] = "";
      continue; 
    }
    
    //ジオコーディングして値を配列に格納する
    var latlng = georeturn(address,0);
    var kekka = latlng.split(",");
    
    dataArray[i][0] = kekka[0];
    dataArray[i][1] = kekka[1];
    dataArray[i][2] = latlng;
  }
  
  //配列データをスプレッドシートに反映させる
  var lastColumn = dataArray[1].length;  //カラムの数を取得する
  var lastRow = dataArray.length;      //行の数を取得する
  sheet.getRange(2,3,finalrow,3).setValues(dataArray);

  //終了メッセージ
  if(flag == 1){
  }else{
    ui.alert("変換が完了しました。");
  }
}

//ジオコーダー用ラッピング関数
function georeturn(address,option){
  //ジオコーダクラスの宣言
  var geocoder = Maps.newGeocoder();
  
  //空のアドレスの場合には、スルーする
  if(address == ""){
    return ""; 
  }
  
  //ジオコーディング実施
  var response = geocoder.geocode(address);
  
  //optionによって値を返す
  var ret = "";
  var result = response.results[0];
  
  switch(option){
    case 0: //全部を取得
      ret = result.geometry.location.lat + "," + result.geometry.location.lng;
      break;
    case 1: //緯度だけ取得
      ret = result.geometry.location.lat;
      break;
    case 2: //経度だけ取得
      ret = result.geometry.location.lng
      break;
    default:
      ret = "オプションが指定されていません"
      break;
  }
  
  return ret;
  
}

//mapsheetデータをmaps側に送り返す関数
function retmapsheet(){
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sheet.getSheetByName("mapsheet");
  var range = ss.getRange("A2:H").getValues();
  
  return JSON.stringify(range);
  
}
  • ジオコーディングする際にUrlfetchAppでGoogle Maps APIに対して投げて取得する方法もあるのですが、UrlfetchAppは1回あたりの呼び出し時におよそ10秒ほど間を空けないとエラーになるので、懸命な手段とは言えません。
  • また、UrlfetchAppでジオコーディング結果を取得する方法は、APIキーが必要です。

HTML側コード

マッププレビューで用いてるコードです。別途Google Maps API Keyのセットアップが必要です。

<!-- Google Maps APIを利用したマップの表示 -->
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false&v=3&language=ja&libraries=visualization,places,geometry&key=ここにAPIキーを入れる""></script>
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
<script type="text/javascript" language="javascript" src="//code.jquery.com/jquery-1.10.2.min.js"></script>
<script type="text/javascript" language="javascript" src="https://officeforest.org/wp/library/maps/html2canvas.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js"></script>
<style type="text/css">
/*マップの表示領域を制御する*/
html, body, #map-canvas { height: 98%; margin: 0; padding: 0; overflow:hidden;}    
/*情報ウィンドウの幅と高さ、横スクロールバーの制御*/
#infodiv{
width:500px;
height:320px;
overflow-x:hidden;
}	
/*情報ウィンドウを若干透過させてみるCSS*/
.gm-style > div > div > div > div > div {
opacity: 0.8;
}
/*テキストボックスを格好良くする*/
#address {
border: solid 2px #29ABE2;
color: #29ABE2;
font-weight: bold;
height: 1.7em;
padding: 0 5px;
font-size: 16px;    
text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
border-radius: 50px;
-moz-border-radius: 50px;
-webkit-border-radius: 50px;
background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#FFF));
background: -moz-linear-gradient(#ddd, #fff);
}
#address:focus {
border: solid 2px #E02B2B;
color: #E02B2B;
height: 1.7em;
padding: 0 5px;
font-size: 16px;
background: -webkit-gradient(linear, left top, left bottom, from(#FFED85), to(#FFF6C0));
background: -moz-linear-gradient(#FFED85, #FFF6C0);
-webkit-animation: 'design04' 1s ease 0s infinite alternate;
}
@-webkit-keyframes 'design04' {
0% {
border: solid 5px #29ABE2;
}
100% {
border: solid 5px #E02B2B;
}
}
</style>
<script type="text/javascript">
var map = null;
var _markerObjs = new Array();		
var stations = [];
//情報ウィンドウを1つだけ開くようにする
var infoWnd = new google.maps.InfoWindow();
//ジオコーダーを追加する
var geocoder = new google.maps.Geocoder();
//マーカーとラジオボタンの制御をするためのコントローラー
var markerController = new google.maps.MVCObject();
//マップレコード用のコンストラクタ
function hospman(_latlng, _lat, _lon, _name, _cate, _icon, _log) {
this.latlng = _latlng;
this.lat = _lat;
this.lon = _lon;
this.name = _name;
this.cate = _cate;
this.icon = _icon;
this.log = _log;
}		
//プレイスマークを画面の中心に持ってくる関数
function setTokyo(latlonman) {
map.setCenter(latlonman);
}			
//ズームインする関数
function zoomIn(zoomman) {
map.setZoom(zoomman);
}	
function getimageman(){
//get transform value
var transform=$(".gm-style>div:first>div").css("transform")
var comp=transform.split(",") //split up the transform matrix
var mapleft=parseFloat(comp[4]) //get left value
var maptop=parseFloat(comp[5])  //get top value
$(".gm-style>div:first>div").css({ //get the map container. not sure if stable
"transform":"none",
"left":mapleft,
"top":maptop,
})
html2canvas($('#map-canvas'),
{
useCORS: true,
onrendered: function(canvas)
{
var dataUrl= canvas.toDataURL('image/png');
location.href=dataUrl //for testing I never get window.open to work
alert(dataUrl);
$(".gm-style>div:first>div").css({
left:0,
top:0,
"transform":transform
})
}
});
}
//検索した住所の位置にマーカーを配置
function codeAddress() {
var address = document.getElementById('address').value;
geocoder.geocode( { 'address': address}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(results[0].geometry.location);
zoomIn(18);
var marker = new google.maps.Marker({
map: map,
position: results[0].geometry.location
});
marker.setVisible(false);
} else {
alert('取得に失敗しました'); //存在しない住所の場合はアラート
}
});
}	
function doClose() {
infoWnd.close();
}	
//マーカーを作成する
function createMarker(params) {
var marker = new google.maps.Marker(params);
//バルーンの中身を構築する
var contentString =  '<div id="infodiv">'+ //インフォウィンドウのサイズを指定
"<strong>" + params.others.name + "</strong><br><br>" + 
"<div><p>" + params.others.log + "</p></div>" +
'</div>'; 
//マーカーがクリックされたら、情報ウィンドウを表示
google.maps.event.addListener(marker, "click", function() {
infoWnd.close();
infoWnd.setContent(contentString);
infoWnd.open(params.map, marker);
setTokyo(params.position);
zoomIn(18); 
});
return marker;
}
function initialize(){
google.script.run.withSuccessHandler(draw).retmapsheet();
}
function draw(response) {
var data = JSON.parse(response);
var numRows = data.length;
//二次元配列にdataを格納する
var cnt = numRows;
//データテーブルにデータを流し込む
for(i=0;i<cnt;i++){
stations[i] = new hospman(data[i][4],data[i][2],data[i][3],data[i][0],data[i][5],data[i][6],data[i][7]);
}
var latlng = new google.maps.LatLng(35.682956,139.766092);
var mapOptions = {
zoom: 12,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP,
scaleControl: true,
navigationControl: true,
mapTypeControl: true,
scrollwheel: true,
navigationControlOptions: {
style: google.maps.NavigationControlStyle.ZOOM_PAN },
mapTypeControlOptions: {
style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
mapTypeIds: [ google.maps.MapTypeId.ROADMAP , google.maps.MapTypeId.HYBRID, google.maps.MapTypeId.TERRAIN],
} 
};   
//マップを表示する
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
//ESCキーで情報ウィンドウを閉じるように実装
google.maps.event.addDomListener(document, 'keyup', function (e) {
var code = (e.keyCode ? e.keyCode : e.which);
if (code === 27) {
infoWnd.close();
}
});
//マップをクリックすると情報ウィンドウを閉じるようにする
google.maps.event.addListener(map,'click',function(){
infoWnd.close();
});
//トラフィックレイヤを表示する
var trafficLayer = new google.maps.TrafficLayer();
trafficLayer.setMap(map);
//センターマーカーを常に表示する
var crosshairShape = {coords:[0,0,0,0],type:'rect'};
var centerm = new google.maps.Marker({
map: map,
icon: 'https://officeforest.org/wp/library/icons/add2.png',
shape: crosshairShape
});
centerm.bindTo('position', map, 'center');
//地図上にマーカーを配置していく
var bounds = new google.maps.LatLngBounds();
var station, latlng;
for ( i = 0; i < stations.length; i++) {
station = stations[i];
latlng = new google.maps.LatLng(station.lat, station.lon);
var pinpon = station.name;
bounds.extend(latlng);
//マーカーのアイコンを用意する
if(station.icon == ""){
var icons = "https://officeforest.org/wp/library/icons/defaultmarker.png";
}else{
var icons = station.icon;
}
var image = {
url: icons,
size: new google.maps.Size(32, 32),
scaledSize: new google.maps.Size(32, 32),
origin: new google.maps.Point(0,0),
anchor: new google.maps.Point(0, 32)
};
//マーカーを作成する
var marker = createMarker({
map : map,
position : latlng,
animation: google.maps.Animation.DROP,
title: pinpon,
icon : image,
others : station				
});
}    
//マーカーが全て収まるように地図の中心とズームを調整して表示
map.fitBounds(bounds);
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
<div id="panel">
<input id="address" type="text" value="" size="45" placeholder="ここに移動したい先を入力(住所や緯度経度、ランドマーク等)">
<input type="button" id="button" class="create" value="ジャンプ" onclick="codeAddress()">&nbsp; <input type="button" id="button" class="action" value="ダウンロード" onclick="getimageman()">
</div>
<p>
<div id="map-canvas">
<center><p><img border="0" src="https://officeforest.org/wp/library/iyakuhin/Corriendo_gato_L.gif"><p><br><font color="red"><b>マップをレンダリング中</b></font></center>
</div>
  • APIキーは<HEAD>にある「https://maps.googleapis.com/maps/api/js」の参照にkey=YOUR_API_KEYという形でパラメータを追加してあげる必要があります。

Google Maps APIキーが必要となりました

有償化されました

2018年7月16日より、いよいよGoogle Maps APIを利用したサービス全てに於いて、APIキーがなければマップが表示されないように仕様変更が実施されました。それまでは、APIキーがなくともマップの表示が可能でした。APIキーは請求アカウントとキーが紐づけされていなければならず、利用自体が有償化されたことになります。

ただし、月額200ドルまでは無償枠が設けられているので、その範囲内であれば課金されないことになります。Map利用の課金枠は以下の通り。かなり複雑な料金体系になっていて、本気で使うには覚悟がいりますね・・・

項目名 月額無料枠(200ドル分) 1,000コールあたりの料金
月の利用量 0−100,000 100,000以上
Mobile Native Static Maps 無制限 $0.00 $0.00
Mobile Native Dynamic Maps 無制限 $0.00 $0.00
Embed 無制限 $0.00 $0.00
Embed Advanced 14,000 loadsまで $14.00 $11.20
Static Maps 100,000 loadsまで $2.00 $1.60
Dynamic Maps 28,000 loadsまで $7.00 $5.60
Static Street View 28,000 panosまで $7.00 $5.60
Dynamic Street View 14,000 panosまで $14.00 $11.20

※ただし上記の料金はMapのみで、ルートと場所の検索クエリに関する課金は別に存在します。詳しい料金表はこちらから。

図:APIキーなしだとこういう表示になってしまう

APIキーの取得

Google Maps APIを使う場合には、以下の手順でAPIキーを手に入れる必要があります。別途クレジットカードが必要です(といっても、上限設定をして課金されないようにセットアップもします)。

  1. Google Maps Platformにアクセスする
  2. 右上にある「使ってみる」をクリックする
  3. Enable Google Maps Platformというダイアログが出ます。今回は、とりあえず全部選択します。continueをクリック
  4. select or Create Projectでは、Google Cloud Consoleでのプロジェクト一覧が出てきます。新規作成をしてみました。nextをクリック。GASのプロジェクトに対しては適用出来ませんでした。
  5. 請求先アカウントを指定しろと言われるので、設定をします。請求先アカウントの作成をクリック。自分はすでにG Suiteユーザであるので、課金アカウントは予め作成済みでしたので、ここは飛ばします。(最初の1回は300ドル分/1年のむ)
  6. Google マッププラットフォームの有効化というダイアログが出て、とりあえず完了です。次へをクリック。
  7. しばらく待つ(結構時間掛かります)とAPIキーが出てくるので、これを控えておきます。大事なキーなので流出などしないように!!
  8. 取得したAPIキーを「https://maps.googleapis.com/maps/api/js?key=xxxxxx」といった形で追記して呼び出すようにする。
  9. You're all setと出たら完了となるので、DONEクリックして閉じる。

図:使用するマップのタイプ。通常はMapsのみでOK

図:プロジェクトの選択画面

図:APIキーは大切に保管しておきましょう。

APIキーに制限を付ける

続けて、認証情報を作成する必要があります。新しいAPIキーを保護する必要がありますと表示されているので、クリックします。これを行わないと、誰かれ問わずキーを使われてしまいます。

  1. キー制限にて「アプリケーションの制限」を選びます。
  2. HTTPリファラー」を選びます。
  3. どこからの呼び出しにだけ応じるか?URLを追加します。通常は埋め込むウェブサイトのURLを入力します。
  4. Google Apps Scriptのウェブアプリケーションやスプレッドシート上のダイアログから呼び出す場合には、一回Keyを入れた状態で呼び出し、F12のデベロッパーコンソールを開きます。
  5. Google Maps JavaScript API error: RefererNotAllowedMapErrorというエラーがあるはず。この中にあるYour site URL to be authorized以下のURLをHTTPリファラーに登録してあげると良い。
  6. 適用してから反映するまで5分ほど時間差があるので注意!
  7. 無事に表示できるか確認する。

図:HTTPリファラーで制限する

図:GASで使うにはひと手間が必要なHTTPリファラーの取得

図:無事に表示できるようになりました。

無料枠以上に利用しないよう制限をつける

Google Cloud Console上にて、まずは一旦ダッシュボードまで戻ります。テストをしてみるとわかりますが、リクエスト数がわかるようになっています。また、17個くらいのAPIがオンになっています。多分全部は要らないので、不要なものは無効化しておくと良いでしょう。しかしこのままでは、リクエストのあるAPIによって課金されかねないので、以下の手順で制限をつけます。

  1. Maps JavaScript APIがリクエストされてるので、その右側の歯車をクリック。
  2. 次の画面で左サイドバーの「割り当て」をクリック。
  3. Map loads per dayの項目が下にあります。無制限になってるので隣の鉛筆アイコンをクリック。
  4. 出てきたダイアログにて無制限のチェックを外してあげる。
  5. 無料の割当が25000くらいなので、20000くらいで値を入れる。確認にチェックを入れて、保存をクリック。
  6. アクセス数が大したことがなければ、これで十分無料枠で毎月利用が出来ます。

図:利用者の多いサイトではさじ加減が重要

プロジェクトを移動

今回の発表直前の2019年4月8日より、Google Apps ScriptからCloud Platform Projectへ直接アクセスが出来なくなりました。これまでにデプロイしてるものについては、これまで通り「リソース」⇒「Google Cloud Platform API ダッシュボード」からアクセスが可能です。

今回の変更はスプレッドシート上で動かすスクリプトやGoogleの拡張サービスを利用しないタイプのスクリプトであれば特に問題はありませんが、「Apps Script API」や「Google Picker API」、「Cloud SQL接続」などGCP上のAPIを利用する場合には以下の手順を踏んで、Google Apps Scriptにプロジェクトを連結する必要があります。これまでは、自動的にGCP上にGoogle Apps Script用のプロジェクトが生成されていたのですが、今後は自分の組織(もしくはGCPプロジェクト)上で作成されたプロジェクトでなければならないということです。詳細はこちらのページを見てください。

連結する手順は以下の通り

  1. Google Cloud Consoleを開く
  2. 左上にある▼をクリックする
  3. ダイアログが出てくるので、新規プロジェクトを作るか?既存のプロジェクトを選択する。この時、G Suiteであれば選択元は「自分のドメイン」を選択する必要があります。
  4. プロジェクト情報パネルから「プロジェクト番号」をコピーする
  5. 対象のGoogle Apps Scriptのスクリプトエディタを開く
  6. 「リソース」⇒「Cloud Platform プロジェクト」を開く
  7. 4.で入手した番号をプロジェクトを変更のテキストボックスに入れて、プロジェクトを設定ボタンをクリックする
  8. 無事に移動が完了すればメッセージが表示されます。
  9. この時、元の自動作成されたプロジェクトはシャットダウンされて消えます。これで設定完了です。

今回のこの変更だと1つ作ったプロジェクトに集約する必要があるので、クォータについてプロジェクト毎のカウントだったので問題なかったものが、集約されることで、クォータに引っ掛かる可能性があります。

図:プロジェクト番号をコピーしておきます

図:プロジェクトを他のプロジェクトに紐付けしました。

図:GCPの拡張サービスを使うには手順が必要になった

Fusion Tablesを使うという選択肢は・・・

自分は1回使っただけで、Google Apps Scriptで使うにはパフォーマンスが悪く、マッピングに関しても、正直自由度が低いということで使っていなかったFusion Tables。G Suitesの見捨てられていたサービスの1つで、データベースを担当!?するような存在でした。結局は殆どメンテされる事もなく、半ば放置されていたので消えるだろうなぁと思っていたら、2019年8月に消えることが確定しました。

SQLみたいな形でデータを取得できる点だけが評価できましたが、パフォーマンスがとっても悪く、スプレッドシートのほうが全然使いやすいので、想定していた通り終焉の日が来ました。ということで、マップ関係ではFusion Tablesを使うのは辞めましょう。せっかく、ライブラリもありましたが。。。そういえば、Google Baseなんてのもありましたね・・・

Mapsに関係なく、Fusion Tables使っちゃってる人は早めに他に移行しましょう。というか、制限が厳しいアプリだったので、まともに使っていた人がいるとは思えないのですが。。。

OpenStreet Mapで実装してみる

今回のスプレッドシートには、オマケとして別プロジェクトにてGoogle MapsではなくOpenStreet Mapのバージョンも含めています。ただ地図を表示するだけなので、マーカーなどの装備を追加していませんが、どうしても、Google Mapsの有償には手が出しにくいという人は、こちらで装備をするのも良いのではないかと思います。

こちらもスプレッドシートで連携して、起動時にポイントデータを流し込めますし、無償で使える地図です。Google Mapsからしたら見劣りはしますが、逆にシンプルで自分で作り込む地図としては良い下地になるかと思います。ソースコードは以下のような感じになります。APIはこちらのページから確認可能です。

ソースコード

<link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css">
<script src="https://openlayers.org/en/v4.6.5/build/ol.js" type="text/javascript"></script>
<style>
.map {
height: 500px;
width: 100%;
}
</style>
<div id="map" class="map">テスト</div>
<script>
//マップ表示
openstreet();
//OpenStreetMapを表示するためのコントローラ
function openstreet(){
var map = new ol.Map({
layers: [new ol.layer.Tile({source: new ol.source.OSM() })],
target: 'map',
view: new ol.View({
center: convertCoordinate(139.710103, 35.646729),
zoom: 18
})
});
//マーカーアイコン準備(配列になっているので複数登録可能)
var icontype=[];
icontype[0]=new ol.style.Style({
image: new ol.style.Icon(/** @type {olx.style.IconOptions} */ ({
color: '#4271AE',
crossOrigin: 'anonymous',
src: 'https://openlayers.org/en/v4.6.5/examples/data/dot.png'
}))
});
var marker=[];
//マーカー位置指定
marker[0] = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([139.710103, 35.646729]))
});
//マーカーにアイコン割り当て
marker[0].setStyle(icontype[0]);
//マップにマーカー追加
map.addLayer(new ol.layer.Vector({source: new ol.source.Vector({
features: [marker[0]]
})}));
}
function convertCoordinate(longitude, latitude) {
return ol.proj.transform([longitude, latitude], "EPSG:4326","EPSG:900913");
}
</script>

表示してみた事例

ネットワークリンク配信してみる

ネットワークリンクとは、Google Earth上でビューを移動した時に、現在見ているエリア等に応じてサーバから情報をリアルタイム要求する為の、KMLの機能の一つ。表示されているエリアやその高さなどに応じてピンやオブジェクトを含んだデータを取得出来るので、非常によく利用されている機能の一つ。

ビューを移動すると東西南北のポイント(緯度経度)に関する情報をサーバに送ってくれるので、それを元にデータを返す仕組みを今回Google Apps Scriptで生成してやろうというのがミッションです。

KML側のコード

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<NetworkLink>
<name>GAS KML Generator</name>
<open>1</open>
<description>KML from GAS</description>
<Url>
<href>ここにGAS側の公開URLを入力?</href>
<refreshInterval>2</refreshInterval>
<viewRefreshMode>onStop</viewRefreshMode>
<viewRefreshTime>1</viewRefreshTime>
</Url>
</NetworkLink>
</kml>

上記のKMLは移動後に1秒後にhrefに設定したURLに対して情報を要求します。URLの最後には必ず「?」を付けることを忘れずに。URLはGAS側のウェブアプリケーションURLを指定します。其のため、ネットワークリンク用にdoGet()が必要になります。

GAS側のコード

//Google Earth側からのリクエストに応答する
function doGet(e) {
//シートIDを取得する
var Properties = PropertiesService.getScriptProperties();
var ssid = Properties.getProperty("mysheetid");
//Google EarthからのGPSデータを取得する
var sheet = SpreadsheetApp.openById(ssid);
var gps = e.parameter.BBOX;
var array = gps.split(",");
//シートに記述する
sheet.getSheetByName("GPSデータ").appendRow(array);
//プレイスマークをフィルタして返す
var place = makeplacemark(array);
return ContentService.createTextOutput(place).setMimeType(ContentService.MimeType.XML);
}
//取得した緯度経度に基づき、データをフィルタしてKMLを生成
function makeplacemark(array) {
//変数の宣言
var kml = "";
//緯度経度情報を分解する
var west = array[0];
var south = array[1];
var east = array[2];
var north = array[3];
//シートIDを取得する
var Properties = PropertiesService.getScriptProperties();
var ssid = Properties.getProperty("mysheetid");
//スプレッドシートの取得
var ss = SpreadsheetApp.openById(ssid).getSheetByName("mapsheet");
var ssdata = ss.getRange("A2:H").getValues();
var slength = ssdata.length;
//冒頭のコードを生成
kml = "<?xml version='1.0' encoding='UTF-8'?>";
kml = kml + "<kml xmlns='http://www.opengis.net/kml/2.2' xmlns:gx='http://www.google.com/kml/ext/2.2'"
+ " xmlns:kml='http://www.opengis.net/kml/2.2' xmlns:atom='http://www.w3.org/2005/Atom'>\n";
kml = kml + "<Document><Folder><name>GAS2KML</name><open>1</open>";
//範囲をフィルタしながらKMLを生成
for(var i = 0;i<slength;i++){
//westの値よりも上且つeastの値よりも下かどうかをチェック
if(ssdata[i][3] >= west && ssdata[i][3] <= east){
//southの値よりも上且つnorthの値よりも下かどうかをチェック
if(ssdata[i][2] >= south && ssdata[i][2] <= north){
kml = kml
+ "<Placemark><description>" + ssdata[i][7] + "</description>\n"
+ "<name>" + ssdata[i][0] + "</name>\n"
+ "<LookAt>"
+ "<longitude>" + ssdata[i][3] + "</longitude>"
+ "<latitude>" + ssdata[i][2] + "</latitude>"
+ "<gx:altitudeMode>relativeToSeaFloor</gx:altitudeMode>"
+ "</LookAt>\n"
+ "<Style><IconStyle><Icon><href>" + ssdata[i][6] + "</href></Icon></IconStyle></Style>\n"
+ "<Point><gx:drawOrder>1</gx:drawOrder>"
+ "<coordinates>" + ssdata[i][3] + "," + ssdata[i][2] + "</coordinates>\n"
+ "</Point></Placemark>\n";
}
}
}
//閉じるコードを追加する
kml = kml + "</Folder></Document></kml>";
//生成KMLコードを返す
return kml;
}
  • 緯度経度の範囲情報は、Google Earth側から受信したe.parameter.BBOXにて、カンマ区切りで取得可能です。
  • 受信した緯度経度範囲の情報をスプレッドシートのGPSデータに記述しています。
  • 緯度経度範囲内にあるプレイスマークだけをmapsheetからフィルタしてKMLを生成しています。
  • makeplacemark関数にてmapsheetからのデータよりKMLを生成しています。
  • 最後にContentServiceにてKMLを出力しています。mimetypeとしてContentService.MimeType.XMLをセットしています。
  • 更にカスタマイズすればラインデータやポリゴンデータなど、また、バルーンの中にデータを表示など可能です。

Navicon連携

Naviconとは、スマフォ用のアプリである「Navicon」の事で、Google Mapsなどのアプリから共有でNaviconに送り込む→Naviconからカーナビにデータを送り込む→カーナビに目的地がセットされるという非常に便利なアプリケーションで、Navicon対応カーナビであれば面倒なカーナビ上での操作をせずに、スマフォから目的地セットが出来てしまいます。

例:navicon用のURL

NaviconにはWeb APIが用意されており、スプレッドシート上の住所をNavicon APIに渡すとアプリを開くためのURL Schemeが返ってくるので、これを元にGoogle Apps Scriptでウェブアプリを構築すれば、自前のドライブ行き先リストを作れてしまいます。URL Schemeはnavicon://といったような特殊なURLになっており、タップするとNaviconが起動するようになります。スマフォでGoogle Apps Scriptで作ったウェブアプリにアクセスして、タップすればOKというわけです。

有償版プランだとタップでNavicon画面を出さずにダイレクトにカーナビに送り込めるようになりますが、結構高額なので企業ユースなどで訪問先顧客リストをGoogleスプレッドシートに作成しておき、カーナビで管理せずに顧客管理の一貫で運用可能になります。

図:URLクリックでNaviconが起動する

事前準備

Navicon APIそのものは無償で利用可能です。セキュアタイプが有償でカーナビにダイレクトに送り込める機能が利用できます。以下の手順でアカウントを作成し、2つのキーを取得します。今回は無償版で進めます。

  1. アカウントを作成しログインする
  2. こちらにアクセスし、オープンタイプの自動発行をクリック
  3. この時点でAPI Keyを取得出来ます。
  4. 次に右上のマイページをクリックして、用途管理をクリック
  5. 新規登録でNaviconを選択し、新規追加をクリック(MapQRコードも作れるみたい)
  6. 用途の必須項目だけ入力して、保存する
  7. すると、用途IDを取得出来ます。
  8. 今回使用するスプレッドシートのメニューより、作業用→設定項目を開く
  9. サイドバーの下のほうにある「API Keyの指定」と「用途IDの指定」でそれぞれ入力登録する
  10. 住所緯度経度変換を実行すると、住所がある場合にはNavicon APIリクエストされてnavicon URLが発行されてスプレッドシートに書き込まれます。

ソースコード

Navicon APIリクエスト

allgeocode関数のコードを修正します。以下のような形にします。

//Navicon Request URL (POST通信)
var nvrequrl = "https://dev.navicon.com/webapi/cmd/navicon/createNaviConURL";
//Navicon APIにアクセスしてURLを発行する
function navgenerate(pointname,address) {
//API Keyを取得する
var prop = PropertiesService.getScriptProperties();
var navapi = prop.getProperty("navapi");
var youto = prop.getProperty("youto");
//formDataを構築する
var formData = "apikey=" + navapi + "&regid=" + youto + "&ver=2.0&name1=" + pointname + "&address1=" + address;
//リクエストオプション
var options = {
'method' : 'post',
'payload' : formData,
'contentType': "application/x-www-form-urlencoded"
};
var response = UrlFetchApp.fetch(nvrequrl, options);
//レスポンスデータからURIを取得する
var json = JSON.parse(response.getContentText());
return json.urlschema;
}
function allgeocode(flag){
//シートデータ取得準備
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("mapsheet");
var range = sheet.getRange("A2:F").getValues();
var num = range.length;
var finalrow = sheet.getLastRow()-1;
//flag値が1の時、サイレントモード
if(flag == 1){
}else{
var ui = SpreadsheetApp.getUi();
var re = ui.alert("緯度経度変換", "住所等データから緯度経度変換しますか?", ui.ButtonSet.YES_NO_CANCEL);
switch(re){
case ui.Button.YES:
//プログレスダイアログを表示
var html = HtmlService.createHtmlOutputFromFile('dialog').setSandboxMode(HtmlService.SandboxMode.IFRAME);
ui.showModalDialog(html, '変換中・・・');
break;
case ui.Button.NO:
ui.alert("変換を中止しました。");
return 0;
break;
case ui.Button.CANCEL:
ui.alert("変換をキャンセルしました。");
return 0;
break;
}   
}
//緯度経度情報格納用二次元配列を作成
//二次元配列を作成する
var msgcnt =3;
var dataArray = new Array(finalrow); 
var navArray = [];
for(i = 0; i < finalrow; i++){
dataArray[i] = new Array(msgcnt); 
}
//住所データ(U列)よりジオコーディングをし、結果をV列に書き込む
for(var i = 0;i<finalrow;i++){ 
//住所を取得
var address = range[i][1];
//地点名称を取得
var pointname = range[i][0];
//住所が空の場合はスルーする
if(address == ""){
dataArray[i][0] = "";
dataArray[i][1] = "";
dataArray[i][2] = "";
navArray[i][0] = "";
continue; 
}
//ジオコーディングして値を配列に格納する
var latlng = georeturn(address,0);
var kekka = latlng.split(",");
dataArray[i][0] = kekka[0];
dataArray[i][1] = kekka[1];
dataArray[i][2] = latlng;
//navicon URLを発行する
var Prop = PropertiesService.getScriptProperties();
var nvapikey = Prop.getProperty("navapi");
if(nvapikey == "" || nvapikey == undefined){
//API Keyが無いのでスルーする
navArray[i][0] = "";
continue;
}else{
//一時配列
var temparray = [];
//Navicon APIにリクエスト
var navurl = navgenerate(pointname,address);
temparray.push(navurl);
//配列にpushする
navArray.push(temparray);
//3秒スリープさせる
//連続で大量にurlfetchappしたばあいに
Utilities.sleep(3000);
}
}
//配列データをスプレッドシートに反映させる
var lastColumn = dataArray[1].length;  //カラムの数を取得する
var lastRow = dataArray.length;      //行の数を取得する
sheet.getRange(2,3,finalrow,3).setValues(dataArray);
//navicon配列データをスプレッドシートに反映させる
if(nvapikey == "" || nvapikey == undefined){
//データ無いので何もしない
}else{
sheet.getRange(2,9,finalrow,1).setValues(navArray);
}
//終了メッセージ
if(flag == 1){
}else{
ui.alert("変換が完了しました。");
}
}
  • nvrequrlはNavicon APIのリクエストエンドポイントURLです。
  • navgenerate関数にて地点名と住所を取得したらAPIリクエストを実行し、naviconのURL Schemeを取得します。
  • 取得したらnavicon URLの列に一気に書き込みます。
  • 但し、UrlfetchAppは10秒間に4リクエスト以上送り込むと429エラーとなることが多いので、Utilities.sleepにて1リクエスト毎に3秒間ウェイトを入れています。
  • Navicon APIは1回につき、5地点送り込めるようです。
  • また、10000回/月の利用上限がついています。有償版は制限がありません。

Naviconリスト表示

Vue + Vuetifyにてスプレッドシートのデータを一覧表示するようにしました。スクリプトのプロジェクトにてNaviconリストを開き、ウェブアプリケーションとして公開すると使えるようになっています(別途このスプレッドシートのIDをコード内の変数:gasheetに記述が必要です)

GAS側コード
//データのあるシートのID
var gasheet = "1cDcP_PEbeAx9Sa5G8ot0Eq_xuyaDzJn8_UUjzc5rYKw";
//Google Earth側からのリクエストに応答する
function doGet(e) {
//paramを元にHTMLをレンダリング
var html = HtmlService.createHtmlOutputFromFile("index")
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.setFaviconUrl('https://drive.google.com/uc?id=1rPShWER6M-z0IxcVSFa6Cx1MLPE-6MCf&.png')
.addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui');
return html;
}
//mapsheetデータをVuetify側に送り返す関数
function gettabledata(){
var sheet = SpreadsheetApp.openById(gasheet);
var ss = sheet.getSheetByName("mapsheet");
var range = ss.getRange("A2:I").getValues();
//タイトル行を取得する
var title = ["name","address","lat","lon","latlon","category","icon","description","url"];
//JSONデータを生成する
return JSON.stringify(range.map(function(row) {
var json = {}
row.map(function(item, index) {
json[title[index]] = item;
});
return json;
}));
}
  • スマフォ表示用に.addMetaTagを使用してHTML Serviceにてウェブアプリケーションを生成しています
  • gettabledata関数にてスプレッドシートのデータをJSON形式にして返しています。
HTML側コード
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<style>
/*  DialogのToolbarを上部に固定化 */
.fixed-bar {
position: sticky;
position: -webkit-sticky; /* for Safari */
top: 0em;
z-index: 9;
}
</style>
<script>
function onSuccess(data){
//vmのrecdataにつっこむ
vm.recdata = JSON.parse(data);
}
</script>
</head>
<body onload="initial()">
<v-app id="app">
<v-card>
<template>
<v-data-table
:headers="headers"
:items="recdata"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar flat class="fixed-bar">
<v-toolbar-title>Naviconリスト</v-toolbar-title>
<v-divider class="mx-4" inset	vertical></v-divider>
<v-spacer></v-spacer>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" @click="openurl(item)">
mdi-airballoon
</v-icon>
</template>
</v-data-table>
</template>
</v-card>
</v-app>
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
<!-- Vuetifyを初期化する -->
<script>
var vm;
//レコードデータ
var recdata = [];
//Body読み込み時に初期化する
function initial() {
//Vue.jsを初期化
vm = new Vue({
el: '#app',
vuetify: new Vuetify(),
name: "table",
data: () => ({
headers: [
{
text: '地名',
align: 'start',
value: 'name',
},
{ text: '所在地', value: 'address' },
{ text: 'アクション', value: 'actions', sortable: false },
],
recdata: recdata,
}),
methods: {
//URLを開く
openurl(item){
//URL Schemeを取り出す
var navlink = Object.assign({}, item)
var chk = navlink.url;
//リンクを開く
window.open(chk, '_blank')
},
}
})
//現在の社員マスタをSQLiteから取得する
google.script.run.withSuccessHandler(onSuccess).gettabledata();
}
</script>
</body>
</html>
  • 今回はスマフォ用インターフェースはVue.jsおよびVuetifyにて構築しています。
  • gettabledata関数にてデータを取得し、vm.recdataでdatatableに値を追加しています。
  • アクションのバルーンアイコンをクリックするとNaviconのURL Schemeが呼び出されて、Naviconが起動する仕掛けです。この場合、新窓で開くようにします。

サンプル:ウェブアプリケーション化してみた

使い方と結果

搭載してる機能

マッププレビュー機能では、以下の機能を搭載しています。

  • Google Drive内の指定のフォルダの画像類をプレイスマークのアイコンとして利用出来るようにしています。
  • トラフィックレイヤーを追加する
  • マップに検索窓を用意して、検索した場所にジャンプする機能
  • センターマーカーを装備
  • 特定の列の数式をマッピング時に自動的に補完する機能(vfatmerge関数)
  • Google Earthからのリクエストから緯度経度情報を取得しスプレッドシートに書き込む機能

セットアップ

また、セットアップが必要です。メニューより「作業用」⇒「セットアップ」を実行します。スプレッドシートのIDがスクリプトプロパティに格納され、プログラム側で利用されます。

またアイコンフォルダの指定では、Google Pickerを利用しているので、Google Cloud ConsoleよりAPIキーを取得してコードに追加する必要があります。追加しないとPicker APIが動きません。マーカーアイコンの追加でこれらの情報を利用しています。

マップ表示プレビュー

APIキーを設定していれば、マッププレビューが見られます。また、今回のスプレッドシートには更に別にmap表示専用というプロジェクトも入れてあります。このスプレッドシートのIDを入れてあげて、map.htmlの中にAPIキーを追記してあげれば、フルスクリーンでマップ表示が可能になっています。

別のプロジェクトなので、HTTPリファラーが別になりますので、制限を掛けている場合には追加で登録してあげましょう。

図:この状態でブログに埋め込むことも可能

ネットワークリンクを表示

GPS2KMLのネットワークリンクのKMLをGoogle Earthに読み込ませて、恵比寿駅に飛ぶと、Google Apps Scriptから出力されたKMLデータが返されて表示されます。現在見ている緯度経度の範囲内にあるプレイスマークのみを絞って表示するので、負荷を低減できるようになっています。

スプレッドシートのデータを利用しているので拡張すればより面白いデータをGoogleスプレッドシートに記述するだけで配信が可能になります。PHPといったサーバー不要でデータ配信が出来るので非常にオススメです。

※バージョンが古いとアイコンが表示されなくなったりするので、最新のGoogle Earthを使いましょう。

※2020/1/31のモバイル版Google Earthのアップデートによって、Android版でもネットワークリンクに対応し、本スクリプトでのプレイスマーク表示ができるようになりました。

図:ネットワークリンクで恵比寿周辺に行くと現れるようになった

図:スマフォ版でも表示できるようになりました

Visualization APIのMap Chartを使ってみる

Google Maps APIとは別に、Visualization APIにはMap Chartと呼ばれるチャート作成機能があります。作成手順がGoogle Maps APIとはずいぶん異なるのですが、表示されるマップはGoogle Maps v3そのものです。ただし、呼び出すJSが異なる為、Google Maps APIのメソッド類は使用する事はできません。

また、Visualization APIにおける Map Chartの呼び出し方が独特で、またリファレンスの記載場所が異なる(というより、Map Chartにあるコードでは動かない)ので、以下のように呼び出しをしなければなりません。要Google Maps API Keyです。

ソースコード

<html>
<head>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script>
var myMapsApiKey = "ここにGoogle Maps API KEYを入れます";
google.charts.load('current', { 'packages': ['map'], mapsApiKey: myMapsApiKey, 'language': 'ja' });
google.charts.setOnLoadCallback(loaddata);
//シートデータをロードする
function loaddata(){
google.script.run.withSuccessHandler(drawMap).retmapsheet();
}
//シートデータをマッピングする
function drawMap(response) {
//カラムの設定
var data = new google.visualization.DataTable();
data.addColumn('number', '緯度');
data.addColumn('number', '経度');
data.addColumn('string', 'ポイント名');
data.addColumn('string', 'icons');
//データをパースする
var json = JSON.parse(response);
var length = json.length;
//取得データをセットする
for(var i = 0;i<length;i++){
//配列を宣言
var tempArray = [];
//データをプッシュする
tempArray.push(json[i][2]);
tempArray.push(json[i][3]);
tempArray.push(String(json[i][0]));
tempArray.push(String(json[i][6]));
//データテーブルに配列を追加
data.addRow(tempArray);
}
var options = {
showTooltip: true,
showInfoWindow: true,
useMapTypeControl: true,
zoomLevel: 16,
};
var map = new google.visualization.Map(document.getElementById('chart_div'));
map.draw(data, options);
};
</script>
</head>
<body>
<div id="chart_div" style="width: 100%; height: 500px"></div>
</body>
</html>
  • 新旧両方のVisualization APIの読み込みが必要なので、loader.jsとjsapiの2つを冒頭で読み込ませてます。
  • オプション設定を細かく設定する事で、マーカーアイコン等を指定する事が可能です。
  • 配列ではなく、DataTableの形式でVisualization APIにデータを渡す必要があります。

ポイント

  • geocoderに投げるとJSON形式で返ってきます。但し、複数返すことも可能なので(例えばキーワードなど)、そこは注意。住所ならば1個しか返ってこないはずなので、result[0]で指定し、rezult.geometry.location.latと指定すれば緯度が取得、lonとすれば経度が取得できます。
  • Googleのジオコーディングサービスは必ずしも住所を入れれば確実な緯度経度に変換されて返って来るわけではありません。日本の住所は非常に複雑なので、精度の低いアバウトな緯度経度で返ってきたり、エラーになって返ってこなかったりします。
  • 建物名などはジオコーディングのキーワードに入れてはなりません。エラーになります。
  • どうしてもエラーになる住所の場合には、その場所をピンポイントにヒットできるキーワードを考えます。ランドマーク名やGoogle検索ワードなどがそれです。一度googleでそれを検索しポイントを教えてくれるかどうかを調べてみると良いでしょう。
  • 【施設名:市までの住所】といった検索ワードも有効です。これはGoogle Maps上で検索したプレイスマークがこのような仕組みを利用しています。
  • 【カテゴリー名:施設名】なんて検索ワードも使えたりしますが、カテゴリー名はマップ上で調べておかないといけません。
  • マップの精度として小数点以下7桁です。それより桁数が小さいと精度が落ちてアバウトなマッピングになってしまいます。
  • マッピングで利用するのは緯度経度の情報なので、片方だけではマッピング出来ません。
  • mapsheetシートの説明文の列は、HTMLコードがそのまま使えます。セルの中にHTMLでコードを記述するとバルーンの中に表示がされます。
  • サイドバーメニューのマーカーアイコンセクションでアイコン登録用のダイアログ表示が出来ます。指定したフォルダ内の画像を自動的にリストアップし、画像をクリックするとURLボックスにURLが組み立てられて入る仕組みになっています。
  • Google Driveの画像に直リンクする為には、https://drive.google.com/uc?export=view&id=というURLに続けて画像ファイルのIDをつなげれば良いです。これで、参照することが出来ます。
  • ESCキーとマップ上でのクリックでバルーンが閉じるようにちょっとだけイベント追加して細工を加えています。
  • Google Maps API v3を使うためには、このURLのようにライブラリへの参照が必要です。
  • 情報ウィンドウのサイズは、#infodivにてCSSで制御することが可能です。
  • 個人的にはこれに追加のマップや、fckeditor入力画面とマップ上でプレイスマークの追加とスプレッドシートへの登録、リバースジオコーディングの実装などをやりたいと思っています。
  • マーカーアイコンは32×32のサイズでスケールするようにオプション指定しています。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)