スプレッドシート連携のGoogle Mapsを作りたい

またひとつ難題が降りかかってきました。それが「グループに所属している企業の所在地マップを作れ」というミッションです。条件は以下の通り。

  1. スプレッドシート連動のGoogle Mapsであること
  2. Google Sitesに貼り付けて利用できるようにすること
  3. 施設のタイプ毎に表示・非表示が出来るようにレイヤ分けすること
  4. スプレッドシートには、施設の名称と住所が入っているだけ。
  5. その他諸々のGoogle Maps API v3を使ったカスタマイズあれこれ

正直な所、標準の機能でこれをやるのは不可能です。Google Appsで作ったマイマップを利用することで割りと簡単に出来るのですが、スプレッドシート連動するわけではなく、一方的なインポートでしかないので、これは最初からアウト・・・また、レイヤ分けする上で、MapEngine Proでない場合、1マップ当たりのレイヤは5枚と非常に厳しく制限されているため、使い物になりません。Proの月額利用料は非常に高いので、そこで考えたのが、かつて流行ったGoogle Maps API v3と、Visualization APIを利用して、スプレッドシートのデータを動的に取得し、XMLガジェットとして貼り付けるという事です。ちなみに、スプレッドシートには、住所から緯度経度を割り出すジオコーディングのスクリプトも仕掛けてあります。

ジオコーディングのスクリプトに関しては別にページを用意してありますので、そちらを参照して下さい。コードのデバッグは、Chromeのデバッガを利用します。

概要

ソースコード

Google Apps Scriptではとても作成できませんので、XMLガジェットを作成して配置します。XMLガジェットの格納先はどこか自由にアクセスできる場所に配置し、参照先ファイルにはパーミッション制限を加えて置きます。また、XMLガジェット内での外部ライブラリなどの参照は、全てhttps://で読めるように書き換えて置きます。

スプレッドシートは、[施設名, クエリワード, 緯度, 経度] の4列を用意してあり、クエリワードを基準にジオコーディングした結果が、緯度経度のそれぞれに格納される仕組みになっています。実際にマッピングで利用するのは、1, 3,4列目の3列分を使用します。

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="施設マップ" />
<Content type="html">
<![CDATA[
<html>
    <head>
        <meta http-equiv="Content-Type"
            content="text/html; charset=utf8">
        <title>Sample Page</title>

<script type="text/javascript" src="https://www.google.com/jsapi"></script>

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<script type="text/javascript"

            src="https://maps.google.com/maps/api/js?sensor=false&language=ja"></script>
        <script type="text/javascript">
        <!--
        var map = null;
        var _markerObjs = new Array();


//corechartのパッケージをダミーロード

google.load("visualization", "1", {packages:["corechart"]});

google.setOnLoadCallback(initialize);

//初期化

function initialize() {

// The URL of the spreadsheet to source data from.

var query = new google.visualization.Query(

'読込対象のスプレッドシートへのURL');

query.send(draw);

}


        function draw(response){

if (response.isError()) {

alert('クエリエラーです');

}

//データを受信し、行数をカウントしておく

var data = response.getDataTable();

var numRows = response.getDataTable().getNumberOfRows();


//二次元配列にdataを格納する

             var stations = [];


//データテーブルにデータを流し込む(1行目だけは飛ばす)

              var cnt = numRows -1;

for(i=1;i<cnt;i++){

var row = [];

row.push(data.getValue(i, 0));

row.push(data.getValue(i, 2));

row.push(data.getValue(i, 3));

stations.push(row);

}

            var element = document.getElementById("map");
            var latlng = new google.maps.LatLng(35.682956,139.766092);

var options = {

zoom: 15,

center: latlng,

mapTypeId: google.maps.MapTypeId.HYBRID,

scaleControl: true,

navigationControl: true,

mapTypeControl: true,

navigationControlOptions: {

style: google.maps.NavigationControlStyle.ZOOM_PAN },

mapTypeControlOptions: {

style: google.maps.MapTypeControlStyle.DROPDOWN_MENU }

};

            map = new google.maps.Map(element,options);

// マーカーを作成

            jQuery.each(stations, function()
            {
                var latlng2 = new google.maps.LatLng(this[1], this[2]);
                _markerObjs.push(new google.maps.Marker({
                    position: latlng2,
                    map: null,
                    title: this[0]
                }));
            });


        }
        //-->


        // 地図にマーカーを追加
        function addMarker()
        {
            jQuery.each(_markerObjs, function()
            {
                this.setMap(map);
            });
        }
        // 地図からマーカーを削除
        function deleteMarker()
        {
            jQuery.each(_markerObjs, function()
            {
                this.setMap(null);
            });
        }
        </script>
    </head>
    <body onload="initialize();">
        <div id="map" style="width:100%; height:90%;"></div>

<input type="button" value="マーカーを追加" onclick="addMarker()" />

<input type="button" value="マーカーを削除" onclick="deleteMarker()" />

    </body>
</html>
]]>
</Content>
</Module>

ソースコード(カテゴリ分けしてみた場合)

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="施設マップ" />
<Content type="html">
<![CDATA[
<html>
    <head>
        <meta http-equiv="Content-Type"
            content="text/html; charset=utf8">
        <title>Sample Page</title>

<script type="text/javascript" src="https://www.google.com/jsapi"></script>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<style type="text/css">#frame{position:relative;width:99%;height:99%;border:1px solid #ccc}#map_canvas{position:absolute;left:0;top:0;width:75%;height:100%}#side_bar{position:absolute;right:0;top:0;width:25%;height:100%;overflow-y:scroll}#side_bar ul{list-style:none;padding:0}#side_bar ul li{margin:0;padding:3px;border-top:1px dotted #ccc;cursor:pointer}

</style>


<link rel="stylesheet" type="text/css" href="https://dl.dropboxusercontent.com/u/3688633/style.css">

<link rel="stylesheet" type="text/css" href="https://dl.dropboxusercontent.com/u/3688633/demo.css">

        <script type="text/javascript"
            src="https://maps.google.com/maps/api/js?sensor=false&language=ja"></script>

<script type="text/javascript"

            src="https://dl.dropboxusercontent.com/u/3688633/modernizr.custom.29473.js"></script>
        <script type="text/javascript">
        <!--
        var map = null;
        var _markerObjs = new Array();

var stations = [];

//情報ウィンドウを1つだけ開くようにする

var infoWnd = new google.maps.InfoWindow();

//マーカーとラジオボタンの制御をするためのコントローラー

var markerController = new google.maps.MVCObject();


//corechartのパッケージをロード

google.load("visualization", "1", {packages:["corechart"]});

google.setOnLoadCallback(initialize);

//コンストラクタの用意

function hospman(_latlng, _lat, _lon, _name, _cate) {

this.latlng = _latlng;

this.lat = _lat;

this.lon = _lon;

this.name = _name;

this.cate = _cate;

}


//初期化

function initialize() {

// The URL of the spreadsheet to source data from.

var query = new google.visualization.Query(

'https://docs.google.com/spreadsheets/d/1KLUIyreOsUKbNUxTfwZ3c1DTPi-jTaqrwJ_Vx_Fw7d4/edit#gid=0');

query.send(draw);

}


        function draw(response){

if (response.isError()) {

alert('クエリエラーです');

}

var data = response.getDataTable();

var numRows = response.getDataTable().getNumberOfRows();


//二次元配列にdataを格納する

var cnt = numRows;

//データテーブルにデータを流し込む

for(i=0;i<cnt;i++){

stations[i] = new hospman(data.getValue(i, 4),data.getValue(i, 2),data.getValue(i, 3),data.getValue(i, 0),data.getValue(i, 5));

}


            var element = document.getElementById("map");
            var latlng = new google.maps.LatLng(35.682956,139.766092);

var options = {

zoom: 15,

center: latlng,

mapTypeId: google.maps.MapTypeId.HYBRID,

scaleControl: true,

navigationControl: true,

mapTypeControl: true,

navigationControlOptions: {

style: google.maps.NavigationControlStyle.ZOOM_PAN },

mapTypeControlOptions: {

style: google.maps.MapTypeControlStyle.DROPDOWN_MENU }

};

            map = new google.maps.Map(element,options);

//ラジオボタンが選択されたら、selectChangedを呼び出すようにイベントを追加する

var i, choise = [

document.getElementById("category1"),

document.getElementById("category2"),

document.getElementById("category3"),

document.getElementById("category4"),

document.getElementById("category5"),

document.getElementById("category6"),

document.getElementById("category7")

];

for (i = 0; i < choise.length; i++) {

google.maps.event.addDomListener(choise[i], "click", selectChanged);

}

//地図上にマーカーを配置していく

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);

bounds.extend(latlng);

var marker = createMarker({

map : map,

position : latlng,

others : station

});

//サイドバーのボタンを作成

createMarkerButton(marker);

}

//マーカーが全て収まるように地図の中心とズームを調整して表示

map.fitBounds(bounds);

//初期値セット

markerController.set("select", "category1");

google.maps.event.trigger(choise[0], "click");

        }
        //-->

//ラジオボタンが選択されたら呼び出される。

//markerControllerの値を更新すると、各マーカーに select_changedイベントが発生する

function selectChanged() {

var selectedVal = this.value;

markerController.set("select", selectedVal);


//選択されたラジオボタンに対応する<ul>を表示する

var i, ul, listNames = ["category1", "category2", "category3", "category4", "category5", "category6", "category7"];

for (i = 0; i < listNames.length; i++) {

ul = document.getElementById(listNames[i] + "_list");

if (listNames[i] === selectedVal) {

ul.style.display = "block";

} else {

ul.style.display = "none";

}

}

}

//マーカーを作成する

function createMarker(params) {

var marker = new google.maps.Marker(params);

//マーカーがクリックされたら、情報ウィンドウを表示

google.maps.event.addListener(marker, "click", function() {

infoWnd.setContent("<strong>" + params.others.name + "</strong>");

infoWnd.open(params.map, marker);

});

// marker.select = markerController.select として連動させる。

// markerController.select が変化すると、marker.select も変化して

// select_changedイベントが発生する

marker.bindTo("select", markerController, "select");

google.maps.event.addListener(marker, "select_changed", changeMarkerVisibility);

return marker;

}

//markerControllerのselectプロパティが変化したら、

//マーカーの表示/非表示を変更する

function changeMarkerVisibility() {

//this は markerを指す

var marker = this;

var others = marker.get("others");

listNames = [["category1","カテゴリ1"], ["category2","カテゴリ2"], ["category3","カテゴリ3"], ["category4","カテゴリ4"], ["category5","カテゴリ5"], ["category6","カテゴリ6"], ["category7","カテゴリ7"]];


//ラジオボタンの選択された値

var selectedVal = marker.get("select");


//selectedValからカテゴリ名をルックアップ

var point = 0;

for(var i = 0;i<listNames.length;i++){


if(selectedVal == listNames[i][0]){

point = listNames[i][1];

break;

}

}


//カテゴリ名とマーカーのカテゴリプロパティの値をチェックして表示/非表示

if(point == others.cate){

marker.setVisible(true);

}else{

marker.setVisible(false);

}

}

//サイドバーにマーカ一覧を作る

function createMarkerButton(marker) {

var others = marker.get("others"),

i, name, ul, li,

listNames = ["category1", "category2", "category3", "category4", "category5", "category6", "category7"];

cateNames = ["カテゴリ1","カテゴリ2","カテゴリ3","カテゴリ4","カテゴリ5","カテゴリ6","カテゴリ7"];


// 各<ul>に選択したカテゴリ名と同一のときだけ作成する

for (i = 0; i < listNames.length; i++) {

name = listNames[i];

nowcate = others.cate;

if (nowcate == cateNames[i]) {

ul = document.getElementById( name + "_list" );

li = document.createElement("li");

li.innerHTML = others.name;

ul.appendChild(li);

//サイドバーがクリックされたら、マーカーを擬似クリック

google.maps.event.addDomListener(li, "click", function() {

google.maps.event.trigger(marker, "click");

});

}

}


}


google.maps.event.addDomListener(window, "load", initialize);

        </script>
    </head>
    <body>


<div id="frame">

<div id="map" style="width:73%; height:100%;"></div>

<section class="ac-container">

<div id="side_bar">

<div>

<label for="category1"><strong>カテゴリ1</strong></label>

<input type="radio" name="accordion-1" value="category1" id="category1" checked="checked">

<ul id="category1_list"></ul>

</div>

<div>

<label for="category2"><strong>カテゴリ2</strong></label>

<input type="radio" name="accordion-1" value="category2" id="category2">

<ul id="category2_list"></ul>

</div>

<div>

<label for="category3"><strong>カテゴリ3</strong></label>

<input type="radio" name="accordion-1" value="category3" id="category3">

<ul id="category3_list"></ul>

</div>

<div>

<label for="category4"><strong>カテゴリ4</strong></label>

<input type="radio" name="accordion-1" value="category4" id="category4">

<ul id="category4_list"></ul>

</div>

<div>

<label for="category5"><strong>カテゴリ5</strong></label>

<input type="radio" name="accordion-1" value="category5" id="category5">

<ul id="category5_list"></ul>

</div>

<div>

<label for="category6"><strong>カテゴリ6</strong></label>

<input type="radio" name="accordion-1" value="category6" id="category6">

<ul id="category6_list"></ul>

</div>

<label for="category7"><strong>カテゴリ7</strong></label>

<input type="radio" name="accordion-1" value="category7" id="category7">

<ul id="category7_list"></ul>

</div>

</div>

</section>

</div>

    </body>
</html>
]]>
</Content>
</Module>

※今回のマップを作るのに使用したスプレッドシート(ジオコーディング機能付き)

実行結果

解説とヒント

  • スプレッドシートの情報をまずは受け取る必要があるため、通常のマップの作業とは冒頭が異なる。
  • initializeで実行するのは、Visualization APIスプレッドシートのデータをdatatableに受け取るルーチンで、callbackであるdraw以下にこれまでのGoogle Mapsの描画ルーチンを書くようにしている。
  • Google Mapsにデータの塊を渡す為に、冒頭にhospmanというコンストラクタを用意している。また、データの入れ物であるstation等は、全てグローバル変数として宣言しておく。
  • 用意したhospmanコンストラクタに、datatableの値をgetValueメソッドを利用して、1レコード単位で入れてあげる(例:data.getValue(i, 2))
  • ラジオボタンのIDなどは今回は統一してある。但し表記は日本語なので、changeMarkerVisibilityやcreateMarkerButton内で、日本語と対応するIDの突合作業をしている。
  • 殆どは、Google Maps APIの流儀そのものをそのまま使用できる。冒頭のデータの取得部分さえきちんと出来れば、問題ない。外部ライブラリも使用できるので、様々な効果を施したり、検索窓を用意したりも可能。
  • スプレッドシートへはこのガジェットからは追記や更新が出来ない(spreadsheet apiなどを使えば出来るのかもしれないけれど)ので、GASで入力用のUIでも用意すると良い。
  • 今回は、装飾としてアコーディオンメニューを採用しているので、ラジオボタンがそのように変更されている。
  • Maps全体をDIVセクションのframeというIDの中に収めているので、サイズなどは状況に応じて調整が必要。frameのCSSは冒頭にある。
  • 余談だが、バルーンのサイズは、マーカー生成時にdivにてサイズ指定を追加してやると、スマートになる。KMLレイヤーの場合には、素材のKMLの各要素のDescription内にDIVでサイズ指定で何かを書き込んでおくと、ちっこいバルーンにならずに済む。

関連リンク