2012年5月14日月曜日

Google Map 上で2点間の経路を検索させる

追記)最近(2016年7月),久々にここの投稿で紹介している具体例を使おうとしたらエラーが出たので,訂正しておいた。 エラーが出た原因は,クリックで始点や終点を指定すると,テキストボックスに入る座標の表示が「(latitude, longitude)」みたいにカッコで囲まれるようになっていたから。いつからカッコで囲まれるようになったんやろ? そのせいで,読み込むときに位置座標と認識してくれなくてエラーになっていた。ちなみに,地名(名古屋駅から岐阜駅みたいに)だとうまくいっていた。そのため,座標の表示の問題だと思って探ったらすぐに原因がわかったので,訂正しておいた。 ついでに小数点以下の桁数を揃える作戦も toFised() 関数を使うように変更しておいた。
1個前の投稿で,Google Map 上で経路を手動で作成する内容の投稿を書いた。その際,いろいろ調べている時に,Google Maps API V3 を使って,2点間の経路の検索をさせることができることがわかり,自分で使いやすいようにページをアレンジしてみた。今回はその報告。今回も一から考えたわけではなくて,他の方の web サイトを参考にした。今回参考にさせていただいたのはおやじプログラマー日記「GoogleMap API V3」 の使い方その10(ルート検索2)というページ。他にも当然Google Maps Javascript API V3 Referenceを参考にしている。

今回も「おやじプログラマー」さんの書かれた内容を自分流にアレンジしている。まずページの内容としては,地図上の点をクリックして出発点と到達点を指定する。場合によっては途中の経由点も指定する。出発点,到着点,経由点はいずれも住所や名称をキーボードから入力して指定することも可能である。その後,経路を求める,とすると,Google Maps API V3 が自動で2点間の経路の検索を行なってくれる。「おやじプログラマー」さんの書かれた内容と異なっているのは,(1) 出発点や到達点以外に経由点を加えた,(2) 出発点や到達点,経由点を地図の点をクリックして指定できるようにした,(3) 出発点,到達点,経由点を住所や名称ではなく,基本的に緯度経度で表すようにした(住所や名称の入力も可能),(4) 高速道路や有料道路を除く,という選択肢を加えた,(5) 検索結果のルートの情報を kml ファイルに挿入できるように数値として表示させるようにした,(6) 経路を通る際に曲がるポイントなどの情報を表示させた,(7) 経路検索後にドラッグでルートを変更可能とした,などである。これらはいずれもGoogle Maps Javascript API V3 Referenceやサンプルページなどを読むと書いてあったものを追加したものである。(3) の出発点,到着点,経由点を緯度経度で与える,というのは,もともと Google Maps API V3 の仕様として,ルート検索のルーチンへの引数として住所や名称でも緯度経度でもよい,となっていたので,マウスクリックで得られた緯度経度をそのまま使うことにした,だけである。

今回もまずは 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 Get Directions</title>
<link href="http://code.google.com/apis/maps/documentation/javascript/examples/default.css" 
               rel="stylesheet" type="text/css">
<script src="http://maps.google.com/maps/api/js?v=3&key=(APIキー)"></script>
<script type="text/javascript">
    var map;
    var centerLatLng = new google.maps.LatLng(35.0, 136.5);
    var rendererOptions = { draggable: true };
    var directionsDisplay = new google.maps.DirectionsRenderer(rendererOptions);
    var directionsService = new google.maps.DirectionsService();

// Direction service 結果のステータス
// OK, MAX_WAYPOINTS_EXCEEDED, NOT_FOUND, INVALID_REQUEST, 
// OVER_QUERY_LIMIT, REQUEST_DENIED, UNKNOWN_ERROR, ZERO_RESULTS
    var direcStat = google.maps.DirectionsStatus; 
    var direcErr = new Array(); //ルート結果のエラーメッセージ
        direcErr[direcStat.INVALID_REQUEST] = "DirectionsRequest が無効";
        direcErr[direcStat.MAX_WAYPOINTS_EXCEEDED] = "経由点がが多すぎます。経由点は 8 以内です。";
        direcErr[direcStat.NOT_FOUND] = "いずれかの点が緯度経度に変換できませんでした。";
        direcErr[direcStat.OVER_QUERY_LIMIT] = "単位時間当りのリクエスト制限回数を超えました。";
        direcErr[direcStat.REQUEST_DENIED] = "このサイトからはルートサービスを使用できません。";
        direcErr[direcStat.UNKNOWN_ERROR] = "不明なエラーです。もう一度試すと正常に処理される可能性があります。";
        direcErr[direcStat.ZERO_RESULTS] = "ルートを見つけられませんでした。";  

    function initialize() {
        var myOptions = {
            zoom: 9,
            center: centerLatLng,
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            scaleControl: true,
            scaleControlOptions: { position: google.maps.ControlPosition.BOTTOM_CENTER }
        }
        map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
        directionsDisplay.setMap(map);
        directionsDisplay.setPanel(document.getElementById("directionsPanel"));
    
        google.maps.event.addListener(map, 'click', function(mouseEvent) {
            setPoints(map, mouseEvent.latLng);
        });
        google.maps.event.addListener(directionsDisplay, 'directions_changed', function() {
            modifyDirectionDataProcess(directionsDisplay.directions);
        });
    }
// ==================================================
    function displayRouteString(result) {
        var strData = '';
        if (document.getElementById("orderLngLat").checked) {
            document.getElementById("orderCoord").textContent = "Lng-Lat";
            document.getElementById("orderCoord").style.color = "red";
        } else {
            document.getElementById("orderCoord").textContent = "Lat-Lng";
            document.getElementById("orderCoord").style.color = "blue";
        }
        for (var j = 0; j < (result.routes.length); j++) {
            for (var i = 0; i < (result.routes[j].overview_path.length); i++) {
                var lat = result.routes[j].overview_path[i].lat().toFixed(9);
                var lng = result.routes[j].overview_path[i].lng().toFixed(9);
                if (document.getElementById("orderLngLat").checked) { strData += lng+','+lat+"\n"; }
                                                               else { strData += lat+','+lng+"\n"; }
            } // for i
        } // for j
        document.getElementById("info_window").value = strData;
    }
// --------------------------------------------------
    function modifyDirectionDataProcess(result) {
        var total = 0;
        var myroute = result.routes[0];
        for (i = 0; i < myroute.legs.length; i++) { total += myroute.legs[i].distance.value; }
        total = total / 1000;
        document.getElementById("total").innerHTML = total + " km";
        displayRouteString(result);
    }
// --------------------------------------------------
    function setPoints(map, latlng) {
        var geocoder = new google.maps.Geocoder();
        var strData;
        if (document.getElementById("start").checked) { 
            document.getElementById("start_pts").value = latlng.lat().toFixed(9)+","+latlng.lng().toFixed(9);
        }
        if (document.getElementById("end").checked) { 
            document.getElementById("end_pts").value = latlng.lat().toFixed(9)+","+latlng.lng().toFixed(9);
        }
        if (document.getElementById("waypoint").checked) {
            strData = document.getElementById("waypoints").value;
            strData += latlng.lat().toFixed(9)+","+latlng.lng().toFixed(9)+"\n";
            document.getElementById("waypoints").value = strData;
        }
    }
// --------------------------------------------------
    function clearAddr() {
        document.getElementById("start_pts").value = '';
        document.getElementById("end_pts").value = '';
        document.getElementById("waypoints").value = '';
    }
// --------------------------------------------------
    function calcRoute() {
        var start = document.getElementById("start_pts").value;
        var end = document.getElementById("end_pts").value;
        var hw_flag;
        var toll_flag;
        var strData = '';

        var waypts = [];
        var waypoints = document.getElementById("waypoints").value;
        var wptsArray = waypoints.split("\n");
        for (var i = 0; i < wptsArray.length; i++) {
            if (wptsArray[i] != '') { waypts.push({location:wptsArray[i], stopover:true}); }
        }

        if (document.getElementById("nonhighway").checked) { hw_flag = true; } else { hw_flag = false; }
        if (document.getElementById("nontollway").checked) { toll_flag = true; } else { toll_flag = false; }

        var request = {
            origin: start, 
            destination: end,
            waypoints: waypts,
            optimizeWaypoints: true,
            avoidHighways: hw_flag,
            avoidTolls: toll_flag,
            travelMode: google.maps.DirectionsTravelMode.DRIVING
        };
        directionsService.route(request, function(response, status) {
            if (status == google.maps.DirectionsStatus.OK) {
                directionsDisplay.setDirections(response);
                displayRouteString(response);
            } else { alert("Directions Service ERROR : "+status+"\n"+direcErr[status]); }
        });
    }
// --------------------------------------------------
</script>
</head>

<body bgcolor="#D0D0D0" onload="initialize()">
  <div id="map_canvas" style="float:left;width:74%;height:100%;"></div>
  <div id="control_panel" 
          style="float:right;width:26%;text-align:left;padding-top:20px;font-size:small;">
    <font class="boldblack" size="+1"> 2点間の経路検索</font> radio ボタン+クリックで指定
    <table border="0" cellpadding="2" cellspacing="0">
      <tr><td colspan="4" style="font-size:small;">
       ・各項目は<font class="blue">住所・名称の直接入力可</font>。
        <font class="black">Lat,Lng (緯度,経度) の方が精度高</font><br>
       ・検索後,<font class="blue">ドラッグによる経路変更可</font>。
        <font class="black">経路点数値データも自動修正</font><br>
      </td></tr>
    </table>
    <hr size="1" class="lightgray">
    <form name="select_points">
      <table border="0" cellpadding="1" cellspacing="0">
        <tr><td style="font-size:small;"> 
          <input type="radio" name="select_points" 
                    style="font-size:small;" id="start" checked>出発点</td>
          <td><input type="text" size="38" id="start_pts"></td></tr>
        <tr><td style="font-size:small;"> 
          <input type="radio" name="select_points" style="font-size:small;" id="end">到着点</td>
          <td><input type="text" size="38" id="end_pts"></td></tr>
        <tr><td style="font-size:small;"> 
          <input type="radio" name="select_points" id="waypoint">経由地</td>
          <td><textarea cols="36" rows="7" id="waypoints" style="font-size:small;"></textarea></td></tr>

        <tr><td colspan="4" align="center" style="font-size:small;">
         <input type="checkbox" id="nonhighway" class="black">高速道路除く  
         <input type="checkbox" id="nontollway" class="black">有料道路除く
        </td></tr>
        <tr><td colspan="4" align="center">
         <input type="button" class="boldred100" onclick="calcRoute();" value="経路を求める">  
         <input type="button" class="boldblue100" onclick="clearAddr();" value="入力値クリア">
        </td></tr>
      </table>
    </form>

    <hr size="1" class="lightgray">
    <table border="0" cellpadding="4" cellspacing="0">
      <tr><td style="font-size:small;"> Info_Window 
          経路検索後は経路の Lng,Lat (<font class="red">緯度経度逆</font>)</td></tr>
      <tr><td>  <textarea cols="40" rows="8" id="info_window" style="font-size:small;">
          </textarea></td></tr>
    </table>

    <hr size="1" class="lightgray">
    <div id="directionsPanel" style="margin:3px; font-size:small; width:340px; 
                                        height:320px; overflow:scroll;">
      <div style="font-size:mideum;">Total Distance: 
        <span id="total" style="font-size:small;"></span></div>
    </div>
  </div>

</body>
</html>

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

メインとなるのは,var map; で始まる JavaScript である。最初に変数の定義として mapcenterLatLngrendererOptionsdirectionsDisplaydirectionsService を定義している。map は地図全体を表す変数であり,26行ほど下で内容が与えられる。centerLatLng は,地図の中心を表す緯度+経度の変数である。ここでは三重県北部を与えている。rendererOptions は検索で得られたルートを表示するレンダラーへのオプションである。directionsDisplay はルートを表示するレンダラー変数であり,Google Maps では, DirectionsRenderer というクラスで表される。directionsServiceGoogle Maps の検索サービスのための変数であり,DirectionsService というクラスで表されている。また,「おやじプログラマー」さんにならって経路検索時のエラーの日本語表示を定義している。

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

関数 displayRouteString は,検索で得られた経路の数値データを地図の右側に置いた Info_window の中に表示させるためのルーチンである。 ここでは,緯度と経度の表示の順を入れ替えれるようにしているので,入れ替えのチェックボタンの内容によって表示内容を入れ替えている。 また,小数点以下の桁数を揃えるために .toFixed() として桁数をコントロールしている。

関数 modifyDirectionDataProcess は,検索で得られた経路のトータルの距離を求める関数であり,その中で経路点の数値データの表示も行なっている。今回も数値データの表示には <textarea> を用いている。検索で得られたルートは複数にわかれている場合があるので,配列になっている。それぞれのルートを表す配列の中の要素 overview_path(これもデータ点の配列)の各要素の緯度経度の情報を数値として書き出している。

関数 setPoints はマウスをクリックした際に,クリックされた地図上の点の緯度経度情報を,出発点や到着点の入力スペースに表示させている。どの点のデータとするかはラジオボタンをつけておいて,選択されている点に緯度経度を書き込んでいる。

関数 clearAddr は出発点,到着点,経由点の入力スペースを空にしている。

関数 calcRoutedirectionsService を使ってルートを検索する関数である。経由点がある場合はそれらを読み取って配列に入れている。また,高速道路を含めない,有料道路を含めない,という選択肢にチェックがあるかどうかをフラグ変数に入れている。そして検索を行う。検索がうまくいった場合はレンダラーでルートを表示させ,また数値データの表示も行なっている。もし検索に失敗したら,警告窓を開いてエラーを表示する。

残りの部分は web ページとしての記述である。特徴としては,<body> の中で読み込まれたら initialize() 関数を実行するように定義しているのと,地図のためのキャンバスや情報窓を定義しているぐらいである。具体的な例はこちらに置いてある。このサイトをそのまま使ってもらえば,経路の検索をすることができる。出発点や到着点は住所や名称を直接入力スペースにいれてもよい。例えば「名古屋駅」などを入れると,Google Maps API V3 が自動で緯度経度に変換してくれるらしい。また検索後に経路をドラッグで変更できるが,変更された点が経由地になるみたいで,高速道路上の点をドラッグで変更しようとすると,高速道路上を指定したつもりでも一般道の点になるみたい。この点は改善してほしいなぁ。ちなみにサンプルサイトはアクセスログを取っているので,その点は了解願いたい。
追記)先頭にも書いておいたが,最近(2016年7月)ここで紹介してる内容を見なおした。 その際にここでの記述も書き換えておいた。さらに,具体例を改良しておいた。 ここにある具体例は,始点と終点をマウスクリックで指定しても,マーカーも何も表示しないので,どの点を指定したかが全然わからない。 そこで,クリックすると赤と青のピンを置くようにした。 しかし,ルート検索すると自動で始点と終点にマークがつくので,ルート検索をしたら赤と青のピンは消すようにしている。

 また,起動したら最初は始点を指定し,その次には終点を,そして追加で経路点を指定したい,と思った。 そこで,起動時にはラジオボタンを始点のところに置き,入力窓も始点のところにカーソルがフォーカスするようにしておいた。 そして,マウスクリックで始点を指定したら,ラジオボタンと入力窓のフォーカスを終点指定のところに移動させることにした。 さらにマウスクリックで終点を指定したら,ラジオボタンは経路点入力を指定し,フォーカスは経路検索開始ボタンに置くことにした。 そうすることで,マウスクリック2回の後でリターンキーを押すと経路検索を始めれるようになっている。
具体例(改良版)

0 件のコメント: