2012年5月6日日曜日

Google Map 上で経路を手動で作成する

以前,GPS Logger のデータを Google Map に表示させる内容の投稿を書いた。例えばGPSの経路ログ表示にGoogle Maps API V3 を使う (その8:InfoWindowの改良) ってやつ。今でも気に入ってツーリングの記録を取る時などに使っているのだが,この間バイクで走りに行った時に GPS のログを取る際にミスをしてしまった。何をしたかというと,途中で GPS Logger の電池を満充電の電池に入れ替えたのだが,その時にすぐにスイッチをいれるのを忘れていた。20分ほど走ってから気づいてスイッチをいれたのだが,その間,しばらく経路が途切れてしまう,という事態が発生してしまった。まぁ,事実として記録し忘れたのだから仕方ないのだが,せっかくの記録だから経路を手動で補完したいと考えた。そこで,以下のようなページを作ったので,今回はその報告を書こう。

ここの内容は私が一から考えたようにも見えるが,実は dinop.com さんの複数の線を描くという記事を参考にした。当初は dinop.com さんの内容をそのままコピーして使えばいいや,と思っていたが,どうも使われている Google Maps API のバージョンが古いみたいで,そのままだとうまく使えなかった。そこで,自分流にアレンジしたものが以下の内容である。内容としては,地図上の点を順にクリックしていくと,経路が描画されていき,その緯度経度の情報が情報窓に数値として表示される,というものであり,Google Mapkml ファイルに点のデータを追加することを念頭に置いている。dinop.com さんの内容と異なっているのは,(1) 線のデータを数値として表示するのだが,表示欄を <textarea> にしておき,そこに数値データをいれてボタンを押すと,数値データから経路を作れるようにした,(2) データのオールクリアを用意した,(3) 経度,緯度の表示の際に,小数点以下の桁数を8ないし9にした,などである。(1) による変更(<textarea> を使ったこと)のおかげで,経路を表す数値データのコピーも楽になった(私のブラウザ環境下,での話であるが…)。何かというと,元々は <div> で領域を確保し,そこに xxxx.innerHTML = yyyy というスクリプトでデータを書かせていたのだが,その場合,データが多くなると全ての数値データを指定してコピーするのが面倒だった。一括で「全てを選択」とすると,他の web 要素まで指定されてしまう。でも,データが多いので全データを指定するにはスクロールして指定して…,などの作業が必要だった。それに対して <textarea> を使うと,中のデータ全部を指定するのに「全てを選択」すればすむから,である。当然,数値で経路を与えてそれを表示させることができるようになったのも便利だけど…。また,当然だがGoogle Maps Javascript API V3 Referenceも参考にしている。

まずは web ページの html ファイルを表示する。
<!DOCTYPE PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html><head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="../main.css">
<title>Google Maps Add Lines</title>
<link href="http://code.google.com/apis/maps/documentation/javascript/examples/default.css" 
               rel="stylesheet" type="text/css">
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
    var map;
    var centerLatLng = new google.maps.LatLng(35.0, 136.5);
    var polylinesArray = [];

    function initialize() {
        var myOptions = {
            zoom: 8,
            center: centerLatLng,
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            scaleControl: true,
            scaleControlOptions: { position: google.maps.ControlPosition.BOTTOM_CENTER },
            scrollwheel: false
        }
        map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

        LineNew(map);
        google.maps.event.addListener(map, 'click', function(mouseEvent) {
            LineExtend(map, mouseEvent.latLng);
        });
    }
// --------------------------------------------------
    function GetCurrentLineNo() {
        return document.getElementById("select_line").options[document.
                        getElementById("select_line").selectedIndex].value;
    }

    function DisplayInfo(myPolylinesArray) {
        var nIndex = GetCurrentLineNo();
        var strData = "";
        for (var i = 0; i < (myPolylinesArray[nIndex].getPath().getLength()); i++) {
            var lat = myPolylinesArray[nIndex].getPath().getAt(i).lat();
            var lng = myPolylinesArray[nIndex].getPath().getAt(i).lng();
            lat = lat * 1000000000;  lat = Math.round(lat);  lat = lat / 1000000000;
            lng = lng * 1000000000;  lng = Math.round(lng);  lng = lng / 1000000000;
            strData += lng + ',' + lat + "\n";
        }
        document.getElementById("latlng_display").value = strData;
    }

    function LineNew(map) {
        var nIndex = GetCurrentLineNo();
        var polyline = new google.maps.Polyline({
                           clickable: false,  editable: false,  geodesic: false,  map: map,  
                           strokeColor:"#FF0000",  strokeOpacity:0.6,  strokeWeight:3,  
                           visible: true,  zIndex: nIndex
                       });
        polylinesArray.push(polyline);

        document.getElementById("text_color").value = polylinesArray[nIndex].strokeColor;
        document.getElementById("text_weight").value = polylinesArray[nIndex].strokeWeight;
        document.getElementById("text_opacity").value = polylinesArray[nIndex].strokeOpacity;
    }

    function LineAdd() {
        var nIndex = document.getElementById("select_line").options.length;
        document.getElementById("select_line").options.length++;
        document.getElementById("select_line").options[nIndex].value = nIndex;
        document.getElementById("select_line").options[nIndex].text = "線 " 
                                              +document.getElementById("select_line").options.length;
        var polyline = new google.maps.Polyline({
                           clickable: false,  editable: false,  geodesic: false,  map: map,  
                           strokeColor:"#FF00FF",  strokeOpacity:0.6,  strokeWeight:3,  
                           visible: true,  zIndex: nIndex
                       });
        polylinesArray.push(polyline);
    }

    function LineExtend(map, myLatLng) {
        var nIndex = GetCurrentLineNo();
        polylinesArray[nIndex].getPath().push(myLatLng);
        polylinesArray[nIndex].setMap(map);
        DisplayInfo(polylinesArray);
    }

    function LineBack() {
        var nIndex = GetCurrentLineNo();
        if (polylinesArray[nIndex].getPath().getLength() > 0) {
            polylinesArray[nIndex].getPath().pop();
            polylinesArray[nIndex].setMap(map);
            DisplayInfo(polylinesArray);
        }
    }

    function LineChange() {
        var nIndex = GetCurrentLineNo();
        document.getElementById("text_color").value = polylinesArray[nIndex].strokeColor;
        document.getElementById("text_weight").value = polylinesArray[nIndex].strokeWeight;
        document.getElementById("text_opacity").value = polylinesArray[nIndex].strokeOpacity;
        DisplayInfo(polylinesArray);
    }

    function LineRefresh() {
        var nIndex = GetCurrentLineNo();
        document.getElementById("info_window").innerHTML=document.getElementById("text_color").value
         +', '+document.getElementById("text_weight").value
         +', '+document.getElementById("text_opacity").value;
        polylinesArray[nIndex].strokeColor = document.getElementById("text_color").value;
        polylinesArray[nIndex].strokeWeight = document.getElementById("text_weight").value;
        polylinesArray[nIndex].strokeOpacity = document.getElementById("text_opacity").value;
        polylinesArray[nIndex].setMap(map);
        DisplayInfo(polylinesArray);
    }

    function LineClear() {
        var nIndex = GetCurrentLineNo();
        var myLatLng = new google.maps.LatLng();
        polylinesArray[nIndex].setPath([myLatLng]);
        polylinesArray[nIndex].getPath().pop();
        polylinesArray[nIndex].setMap(map);
        DisplayInfo(polylinesArray);
    }

    function LineExtendRead() {
        var nIndex = GetCurrentLineNo();
        var myLatLng = new google.maps.LatLng();
        var myLatLngBounds = new google.maps.LatLngBounds(); // variable for the region to display
// clear array first
        polylinesArray[nIndex].setPath([myLatLng]);
        polylinesArray[nIndex].getPath().pop();

        var lineData = document.getElementById("latlng_display").value;
        var singlelines = lineData.split("\n");
        for (i in singlelines) {
// empty line makes an error
            if (singlelines[i] != '') { 
                var yy = singlelines[i].split(",");
                myLatLng = new google.maps.LatLng(yy[1],yy[0]);
                document.getElementById("info_window").innerHTML = myLatLng;
                polylinesArray[nIndex].getPath().push(myLatLng);
                myLatLngBounds.extend(myLatLng); // display region must include all line points
            }
        }
        map.fitBounds(myLatLngBounds); // fit map zoom to include all line points
        polylinesArray[nIndex].setMap(map);
        DisplayInfo(polylinesArray);
    }
// --------------------------------------------------
></script>
</head>

<body bgcolor="#D0D0D0" onload="initialize()">

<div id="map_canvas" style="float:left; width:76%; height:100%;"></div>
<div id="control_panel" style="float:right;width:24%;text-align:left;padding-top:20px">

<form name="form_lines">
<table border="0" cellpadding="2" cellspacing="0">
<tr><td colspan="4" style="font-size:small;">
 <font class="boldblack" size="+1">マウスクリックによる経路作成</font><br>
 ・経路データは Lng,Lat (経度,緯度) 数値データとして表示<br>
 ・Lng,Lat (経度,緯度) 数値をデータ配列に読込可<br>
</td></tr>

<tr><td colspan="4"><hr size="1" class="lightgray"></td></tr>

<tr><td valign="top" colspan="2">
<select name="select_line" id="select_line" onChange="LineChange();" >
<option value="0" selected>線 1</option></select>
<input type="button" value='新規ライン追加' id="button_new" onclick="LineAdd()">
</td></tr>

<tr><td colspan="4"><hr size="1" class="lightgray"></td></tr>
<tr><td valign="top">
<input type="button" value='選択ライン All クリア' id="button_back" onclick="LineClear()">
</td>
<tr><td colspan="3">
<input type="button" value='選択ライン・最終点を削る' id="button_back" onclick="LineBack()">
<br> </td></tr>

<tr><td colspan="4">
<table border="0" cellpadding="0" cellspacing="0">
<tr><td align="center" colspan="4">
<input type="button" value='下記パラメータで経路線の再描画' id="button_refresh" 
     onclick="LineRefresh()" class="boldgreen100">
</td></tr>
<tr><td align="center" style="font-size:small;">色名</td>
<td><input type="text" name="text_color" id="text_color" size="10"></td>
    <td align="center" style="font-size:small;">太さ</td>
<td><input type="text" name="text_weight" id="text_weight" size="4"></td>
    <td align="center" style="font-size:small;">不透明度</td>
<td><input type="text" name="text_opacity" id="text_opacity" size="4"></td></tr>
</table>
</td></tr>

<tr><td colspan="4"><hr size="1" class="lightgray"></td></tr>
<tr><td colspan="4" style="font-size:small;">
<input type="button" value='下記 Lng,Lat 値をデータへ読込' id="button_back" 
    onclick="LineExtendRead()" class="boldblue100">
</td></tr>

<tr><td colspan="4" style="font-size:small;">LngLat Display</td></tr>
<tr><td colspan="4">
<textarea cols="55" rows="22" id="latlng_display" style="font-size:small;"></textarea>
</td></tr>

<tr><td colspan="4"><hr size="1" class="lightgray"></td></tr>

<tr><td colspan="4" style="font-size:small;">Information Window
<div id="info_window" style="width:300px; height:100px; overflow:scroll; padding:3px; 
        background-color:white; border:1px solid black; position absolute; font-size:small;">
</div></td></tr>
</table>
</form>

<hr size="2" class="lightgray">
<input type="button" value="CLOSE Window" 
    onClick="if(document.all){window.opener=true;}window.close()" class="blue">
</div>
</body>
</html>

何をしているかを説明しよう。上から順に見て欲しい。最初は web ページのおまじないみたいなのが並んでいる。<html>や<head>などではじまり,mata タグでいろいろと記載がある。基本的に Google Maps API を使う際のおまじないみたいなものだと思ってほしい。

メインとなるのは,var map; で始まる JavaScript である。最初に変数の定義として mapcenterLatLngpolylinesArray を定義している。mapは地図全体を表す変数であり,12行ほど下で内容が与えられる。centerLatLng は,地図の中心を表す緯度+経度の変数である。ここでは三重県北部を与えている。polylinesArray はクリックを繰り返して得られる経路のための変数である。Google Maps では,経路は Polyline というクラスで表される。今回は複数の経路線を表示させるために,Polyline クラスの変数の配列を用意している。定義では単に「配列」として定義しておき,具体的な中身は後で代入している。

その次の initialize() は,web ページが読まれた時に実行されるスクリプトである。その中では,(1) 地図表示のオプションの定義,(2) 地図の表示,(3) 最初の経路線の作成,(4) クリックした際の処理の定義,をおこなっている。地図のオプションとしては,適当な縮尺で表示させたいので,zoom: 8 としている。また,中心を centerLatLng で与え,縮尺の変更を行うスライドバーを定義している。地図の表示は
        map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
で行なっている。これは変数 mapGoogle MapsMap というクラスを代入している。表示領域は map_canvas という id を持った領域(下の方で定義)に表示される。マウスクリックした際の処理は,google.maps.event.addListerner... の部分である。内容としては,変数 map で与えられる地図でクリックされたら,LineExtend という関数(後で定義)を実行する,というもので,LineExtend という関数を実行する際にマウスでクリックしたポイントの緯度経度の値を引数として渡している。

続く部分が実際に経路線を描いていく部分である。経路線は地図上に描画されるだけではなく,各点の緯度と経度の値を情報窓に順に表示するようにしている。まず,関数 GetCurrentLineNo だが,これは複数描画可能な経路線の何番を今処理しているかを得る関数である。経路線の番号は後の方で web のエレメントとして表示させ,選択させている。その選択部分に "select_line" という id を与えていて,その内容を読み出す関数である。

関数 DisplayInfo は,作業中の経路線の緯度経度情報を情報窓に数値として表示するための関数である。nIndex で与えられる番号の経路線の内容を数値として表示させている。polyline 変数の getPath() メソッドで経路の緯度経度の配列を得て,さらに getAt(i) メソッドでその中の i 番目の点の緯度経度を得ている。一度 100000000 をかけてから,再び 100000000 で割っているのは,小数点以下の桁を最大8桁にするためである。また,緯度経度は Google Mapskml ファイルでの記述にあわせて,経度,緯度の順に表示している。

関数 LineNew は,新規に経路線を定義している。経路線を定義するには,google.maps.Polyline というクラスの変数を新たに用意し,配列 polylinesArray に入れている。線の色や幅,透明度(不透明度)はオプションとして与えている。ここでは,色はオレンジ,不透明度は「0.6」(0で透明,1で不透明),線の幅は3としている。後は,色や線幅を変更する入力スペースに現在の設定値を書き込んでいる。

関数 LineAdd は関数 LineNew と似ているが,経路線を新規に追加する関数であり,線の本数を増やす作業が追加されている。また,線の色は紫としている。その他は関数 LineNew と同じである。

関数 LineExtend は,クリックで点が追加された場合の処理である。GetCurrentLineNo 関数で経路線番号を得て,その番号の経路線の経路にクリックされた点の緯度経度を付け加える。そして経路を地図に表示し,情報窓に経路の緯度経度の数値を表示している。

関数 LineBack は点が1点以上あれば,経路線から最後の点を取り去るルーチンであり,取り去ったら描画しなおして,数値情報を記載している。

関数 LineChange は,作業する経路線を変更する作業であり,線の色や幅などの情報を選ばれた線のものに置き換え,数値情報を選ばれた線の情報に書きなおしている。地図上にはすでに線が描画されているので,線の描画の処理は含まれていない。

関数 LineRefresh は,変更された線の色や幅,不透明度を描画に反映させるための関数であり,与えられた経路線の情報を書き換えて描画しなおしている。

関数 LineClear は,入力されたラインデータをクリアするための関数である。もっといい作戦があるといいのだが,よくわからないので,こんなことをしている。もっと簡単なメソッドがあればお教え願いたい。

関数 LineExtendRead は,ラインデータ表示領域に数値を入力し,それを読み込むための関数である。ポイントとしては表示領域を <textarea> で作っている点である。

残りの部分は web ページとしての記述である。特徴としては,<body> の中で読み込まれたら initialize() 関数を実行するように定義しているのと,地図のためのキャンバスや情報窓を定義しているぐらいである。具体的な例はこちらに置いてある。ここには記述されていない追加もなされているが,その点に関しては,ソースを読んで考えてみてほしい。このサイトをそのまま使ってもらえば,手動で経路を得ることができる。もちろん自分で組み直してもらった方が使いやすいかもしれない。ちなみにサンプルサイトはアクセスログを取っているので,その点は了解願いたい。

0 件のコメント: