PHPで地図上の距離計測

(1/1)
PHPで大圏航路を描く」を出発点にして、今回は地図上の複数地点を結ぶ大圏航路を描き、その距離を計算するプログラムを作ってみる。あわせて、地図サービスの経路探索サービスを利用し、大まかな移動距離・移動時間も表示できるようにする。
複数地点に配置するマーカーをドラッグで移動するインタラクティブな処理はJavaScriptに任せ、PHP側は住所・ランドマーク検索のみとする。地図サービスは、Googleマップと地理院地図・OSM(オープンストリートマップ)を切り替えて使えるようにする。
Mapion キョリ測」より機能が多い分、プログラムもかなり長くなった。

(2023年8月12日)国土地理院ジオコーディングAPIを利用できるようにした.検索キーの最小・最大長が指定できるようにした.

目次

サンプル・プログラムの実行例

PHPで地図上の距離計測
Googleマップ:距離計測
PHPで地図上の距離計測
Googleマップ:経路表示
PHPで地図上の距離計測
オープンストリートマップ:距離計測
複数のマーカーを結ぶ大圏航路を赤い曲線(近距離ではほぼ直線に見える)で描き、地点間の距離を一覧表に示す。
Googleマップでは「経路」ボタンをクリックすると経路を表示し、「距離」ボタンをクリックすると再び距離計測表示に戻る。

YOLPコンテンツジオコーダAPI(Yahoo!JAPAN)は、世界のキーワードから緯度・経度を求める機能が弱い。たとえば「サンティアゴ」でキーワード検索すると、Google Geocoding API チリの首都を第一候補として返すが、YOLPコンテンツジオコーダAPIはドミニカ共和国の県都しか返さない。
そこで、地図上のマーカーをドラッグすることで、自動的に航路を再描画し、距離を再計算する機能を採り入れた。

サンプル・プログラムのダウンロード

圧縮ファイルの内容
getDistance.phpサンプル・プログラム本体
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
getDistance.php 更新履歴
バージョン 更新日 内容
2.3.0 2023/08/12 検索キーの最小・最大長の指定
2.2.0 2023/08/12 国土地理院ジオコーディングAPIを追加
2.1 2021/12/04 Leaflet対応
2.0 2021/11/06 PHP8対応,リファラ・チェック改良,Yahoo!マップ終 了
1.21 2019/04/25 IE11動作不具合改善,その他bug-fix
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
6.3.1 2023/07/09 bug-fix
6.3.0 2023/07/02 getPointsGSI()追加
6.2.0 2023/07/02 ip2address()追加
6.1.0 2022/12/30 ip2address()追加
6.0.4 2022/12/13 PHP8.2対応
pahooInputData.php 更新履歴
バージョン 更新日 内容
1.3.0 2023/07/11 roundFloat() 追加
1.2.0 2023/04/22 exitIfLessVersion() 追加
1.1.2 2023/02/05 validString() 修正
1.11 2022/07/03 isCommandLine() 修正
1.1 2022/06/04 getValidString() 修正

準備:pahooGeoCode クラス

  37: class pahooGeoCode {
  38:     var $items;     //検索結果格納用
  39:     var $error;     //エラー・フラグ
  40:     var $errmsg;    //エラー・メッセージ
  41:     var $hits;      //検索ヒット件数
  42:     var $webapi;    //直前に呼び出したWebAPI URL
  43: 
  44:     //Google Cloud Platform APIキー
  45:     //https://cloud.google.com/maps-platform/
  46:     //※Google Maps APIを利用しないのなら登録不要
  47:     var $GOOGLE_API_KEY_1 = '**************************';   //HTTPリファラ用
  48:     var $GOOGLE_API_KEY_2 = '**************************';   //IP制限用
  49: 
  50:     //Yahoo! JAPAN Webサービス アプリケーションID
  51:     //https://e.developer.yahoo.co.jp/register
  52:     //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  53:     var $YAHOO_APPLICATION_ID = '*****************************';

地図サービスを利用するために、クラスファイル "pahooGeoCode.php" を使用する。組み込み関数  require_once  を使って読めるディレクトリに配置する。ディレクトリは、設定ファイル php.ini に記述されているオプション設定 include_path に設定しておく。
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

地図や住所検索として Google を利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を、Yahoo!JAPAN を利用するのであれば、Yahoo! JAPAN Webサービス アプリケーションIDが必要で、その入手方法は「Yahoo!JAPAN デベロッパーネットワーク - WebAPIの登録方法」を、それぞれ参照されたい。

準備:地図サービスの選択

  63: //地図描画サービスの選択
  64: //    0:Google
  65: //    2:地理院地図・OSM
  66: define('MAPSERVICE', 2);
  67: 
  68: //住所検索サービスの選択
  69: //    0:Google
  70: //    1:Yahoo!JAPAN
  71: //   11:HeartRails Geo API
  72: //   12:OSM Nominatim Search API
  73: //   13:国土地理院ジオコーディングAPI
  74: define('GEOSERVICE', 1);
  75: 
  76: //逆ジオコーディングサービスの選択
  77: //    0:Google
  78: //    1:Yahoo!JAPAN
  79: //   11:HeartRails Geo API
  80: //   21:簡易ジオコーディングサービス
  81: define('REVGEOSERVICE', 1);

表示する地図は、Googleマップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVIC に値を設定すること。
住所検索サービスは、GoogleYahoo!JAPANHeartRails Geo APIOSM Nominatim Search API国土地理院ジオコーディングAPI から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
逆ジオコーディングサービスは、GoogleYahoo!JAPANHeartRails Geo API簡易ジオコーディングサービスから選べる。あらかじめ、定数 REVGEOSERVICE に値を設定すること。

PHPとJavaScriptの役割分担

マーカーのドラッグなど、インタラクティブな処理をPHP(サーバサイド)に委ねるとレスポンスが悪くなるので、JavaScriptで実装した。両者の役割分担は下表の通りである。
No. 処理系 関数名 機能
1 PHP pahooGeoCode::searchPoint3 住所・ランドマークから緯度・経度を求める。
2 PHP makeCommonBody HTML BODYを作成する。
3 JavaScript greatCircleDistance 2地点間の大圏航路距離を求める。
4 JavaScript greatCircleSailing 2地点間の大圏航路軌跡を求める。
5 JavaScript dragendMarker マーカーのドラッグエンド処理。
6 JavaScript addMarker 軌跡の最後にマーカーを1つ追加する。
7 JavaScript delMarker 軌跡の最後のマーカーを1つ削除する。
8 JavaScript fitting 地図を最適サイズにフィットする。
9 JavaScript stringifyMarkers マーカーの位置情報をJSON文字列に変換して代入する。
10 JavaScript drawRoute 経路探索し、マーカー・軌跡を消去する。
11 JavaScript delRoute 経路を消去し、マーカー・軌跡を再描画する。
12 JavaScript writeDtable 距離TABLEを表示する。
13 JavaScript writeRtable 経路TABLEを表示する。
14 JavaScript initRtable 経路TABLEを初期化する。

解説:初期値

  62: //マップの表示サイズ(単位:ピクセル)
  63: define('MAP_WIDTH',  600);
  64: define('MAP_HEIGHT', 400);
  65: //マップID
  66: define('MAPID', 'map_id');
  67: //初期値
  68: define('FLAG_DISTANCE', TRUE);          //TRUE:距離明細を表示,FALSE:非表示
  69: define('MAX_MARKERS',   20);            //マーカーの最大数
  70: define('DEF_LONGITUDE0', 139.766667);   //地図中心(経度)
  71: define('DEF_LATITUDE0',  35.681111);    //    (緯度)
  72: define('DEF_QUERY',     '東京駅');      //検索クエリ
  73: define('DEF_CAT',       'landmark');    //カテゴリ
  74: define('DEF_TYPE',      'roadmap');     //マップタイプ
  75: define('DEF_ZOOM',      10);            //ズーム
  76: define('DEF_POINTS',    '[]');          //マーカー位置JSON
  77: define('LINE_COLOR',    'FF0000');      //軌跡の色(RGB)
  78: define('LINE_WEIGHT',   3);             //軌跡の太さ
  79: 
  80: //住所検索時センタリング
  81: define('CENTERING', TRUE);      //TRUE:センタリングする/FALSE:しない
  82: 
  83: //経路探索
  84: if (MAPSERVICE == 1) {
  85:     define('SEARCH_ROUTE', TRUE);       //TRUE:経路探索する/FALSE:しない
  86: else {
  87:     define('SEARCH_ROUTE', FALSE);
  88: }
  89: define('ROUTE_COLOR',   '0000FF');  //経路の色(RGB)
  90: define('ROUTE_WEIGHT',  3);         //経路の太さ

各種のデフォルト・パラメータは、これらの定数によって設定されている。自由に変更できる。
定数 FLAG_DISTANCE が TRUE の時は、経由地点間の距離明細を一覧表に表示する。一覧表の行数が不定になるためページのレイアウトが崩れるような、FALSE にすることで、距離の合計値のみを表示するようになる。
定数 MAX_MARKERS はマーカーの最大数を定義している。プログラム上、マーカーの数に上限はないのいだが、処理系に負荷をかけない程度の数に抑えておいた方が無難だろう。

定数 CENTERINGTRUE にすると、住所・ランドマーク検索後に、ヒットした位置に地図がセンタリングする。
定数 SEARCH_ROUTETRUE にすると、地図サービスの経路探索機能を利用できるようにする。前述のように、Googleマップでは経路探索機能は課金率が高いことから、FALSE にすることで、経路探索は利用せず、関連スクリプトも出力しなくなる。

解説:大圏航路の軌跡

 373:     /**
 374:      * 2地点間の大圏航路軌跡を求める
 375:      * @param   float long_a, lati_a  A地点の経度,緯度(世界測地系)
 376:      * @param   float long_b, lati_b  B地点の経度,緯度(世界測地系)
 377:      * @return  array points  軌跡の座標を格納
 378:      *                      [n]['longitude'] 軌跡の経度(世界測地系)
 379:      *                      [n]['latitude']  軌跡の緯度(世界測地系)
 380:     */
 381:     function greatCircleSailing(long_a, lati_a, long_b, lati_b) {
 382:         let points = Array();
 383:         lati_a = deg2rad(lati_a);
 384:         long_a = deg2rad(long_a);
 385:         lati_b = deg2rad(lati_b);
 386:         long_b = deg2rad(long_b);
 387: 
 388:         let l1 = (long_a >= 0) ? long_a : 2 * Math.PI + long_a;
 389:         let l2 = (long_b >= 0) ? long_b : 2 * Math.PI + long_b;
 390:         let dd = l2 - l1;
 391:         let tt = 0.01;          //経度方向の増分
 392:         if (dd < 0) {
 393:             dd = Math.abs(dd);
 394:             tt = -tt;
 395:         } else if (dd > Math.PI) {
 396:             [lati_a, lati_b] = [lati_b, lati_a];
 397:             [long_a, long_b] = [long_b, long_a];
 398:             dd = 2 * Math.PI - dd;
 399:         }
 400:         let st = 0.0;
 401:         let cnt = 0;
 402: 
 403:         //軌跡の計算
 404:         let latitude  = lati_a;
 405:         let longitude = long_a;
 406:         while (st < dd) {
 407:             if (latitude  >= 0.5 * Math.PI)     latitude -= Math.PI;
 408:             if (longitude >= 1.0 * Math.PI)     longitude = longitude - Math.PI;
 409:             points[cnt] = {
 410:                 "latitude"  : rad2deg(latitude),
 411:                 "longitude" : rad2deg(longitude)
 412:             };
 413:             longitude += tt;
 414:             if (longitude >= Math.PI)   longitude = longitude - 2 * Math.PI;
 415:             latitude = (Math.sin(lati_a) * Math.sin(long_b - longitude)) / (Math.cos(lati_a) * Math.sin(long_b - long_a)) + (Math.sin(lati_b) * Math.sin(long_a - longitude)) / (Math.cos(lati_b) * Math.sin(long_a - long_b));
 416:             latitude = Math.atan(latitude);
 417:             if (Math.sin(lati_a) / Math.sin(lati_b) < 0)    latitude += Math.PI;
 418: 
 419:             st += Math.abs(tt);
 420:             cnt++;
 421:         }
 422:         points[cnt] = {
 423:             "latitude"  : rad2deg(lati_b),
 424:             "longitude" : rad2deg(long_b)
 425:         };
 426:         cnt++;
 427: 
 428:         return points;
 429:     }

大圏航路の軌跡(座標)を求める関数 greatCircleSailing は、「PHPで大圏航路を描く」で紹介したPHPメソッド greatCircleSailing をJavaScriptに移植したものである。
座標数ではなく、座標そのものの配列を戻す形に変更している。

解説:大圏航路の距離

 354:     /**
 355:      * 2地点間の大圏航路距離を求める
 356:      * @param   float long_a, lati_a  A地点の経度,緯度(世界測地系)
 357:      * @param   float long_b, lati_b  B地点の経度,緯度(世界測地系)
 358:      * @return  float 大圏航路距離(km)
 359:     */
 360:     function greatCircleDistance(long_a, lati_a, long_b, lati_b) {
 361:         lati_a = deg2rad(lati_a);
 362:         long_a = deg2rad(long_a);
 363:         lati_b = deg2rad(lati_b);
 364:         long_b = deg2rad(long_b);
 365: 
 366:         //距離の計算
 367:         let ll = Math.abs(long_b - long_a);
 368:         let distance = 6371.0 * Math.acos(Math.sin(lati_a) * Math.sin(lati_b) + Math.cos(lati_a) * Math.cos(lati_b) * Math.cos(ll));
 369: 
 370:         return distance;
 371:     }

大圏航路の距離を求める関数 greatCircleDistance は、「PHPで大圏航路を描く」で紹介したPHPメソッド greatCircleDistance をJavaScriptに移植したものである。

解説:距離TABLE、経路TABLE

 252:     /**
 253:      * 距離TABLEを表示
 254:      * @param   Array arr 距離配列
 255:      * @return  なし
 256:     */
 257:     function writeDtable(arr) {
 258:         let flag={$flagDistance};   //TRUE:距離明細を表示/FALSE:非表示
 259:         let d0, d1, d2, d3;
 260:         let html = '';
 261: 
 262:         //距離明細(地点間)
 263:         if (flag) {
 264:             let pcount = Number(document.getElementById('pcount').value);
 265:             for (let i = 1; i < pcount; i++) {
 266:                 d0 = arr[i];
 267:                 d1 = roundf(d0, 1);
 268:                 d2 = roundf(km2mi(d0), 1);
 269:                 d3 = roundf(km2nm(d0), 1);
 270:                 html += '<tr>';
 271:                 html += '<th>' + i + '</th>';
 272:                 html += '<td>' + d1 + '</td>';
 273:                 html += '<td>' + d2 + '</td>';
 274:                 html += '<td>' + d3 + '</td>';
 275:                 html += '</tr>';
 276:             }
 277:         }
 278:         //距離合計
 279:         d0 = sum(arr);
 280:         d1 = roundf(d0, 1);
 281:         d2 = roundf(km2mi(d0), 1);
 282:         d3 = roundf(km2nm(d0), 1);
 283:         html += '<tr>';
 284:         html += '<th>合計</th>';
 285:         html += '<td>' + d1 + '</td>';
 286:         html += '<td>' + d2 + '</td>';
 287:         html += '<td>' + d3 + '</td>';
 288:         html += '</tr>';
 289: 
 290:         //TABLEへ表示
 291:         let dtable = document.getElementById('dtable');
 292:         dtable.innerHTML = html;
 293:     }

ユーザー関数 writeDtable は、関数 greatCircleDistance によって計算した地点間距離をHTMLに埋め込むものである。
JavaScriptにはPHPの  sprintf  に相当する機能がないため、小数点以下を四捨五入して文字列で返す関数 roundf を用意した。

ユーザー関数 writeRtablex は、後述する経路探索が有効な場合に呼び出されるもので、地図サービスから返ってくる、大まかな経路総距離と経路総時間を表示する。総時間は分で渡し、ユーザー関数 hhmm を介して「時分」に変換してから表示する。

解説:Google JavaScriptマップ用スクリプト(マーカー・軌跡)

 453: /**
 454:  * Google JavaScriptマップ用スクリプト(マーカー・軌跡)を生成する.
 455:  * @param   string $points マーカーの初期座標(JSONテキスト)
 456:  * @return  string JavaScript
 457: */
 458: function jsGmap($points) {
 459:     $maxMarkers = MAX_MARKERS;          //マーカーの最大数
 460:     $color  = '#' . LINE_COLOR;
 461:     $weight = LINE_WEIGHT;
 462: 
 463:     $js =<<< EOT
 464:     let Dmarkers  = Array();            //マーカー格納用
 465:     let Dlines    = Array();            //軌跡格納用
 466:     let Distances = Array();            //距離格納用
 467:     let EventListner;                   //イベントハンドラ格納用
 468: 
 469:     //マーカーの初期状態
 470:     let points = JSON.parse('{$points}');
 471:     document.getElementById('pcount').value = 0;
 472:     for (let i = 0; i < points.length; i++) {
 473:         _addMarker(points[i].latitude, points[i].longitude);
 474:     }
 475: 
 476:     //イベント設定
 477:     EventListner = google.maps.event.addListener(map, 'click', addMarker);
 478:     document.getElementById('delmarker').addEventListener('click', delMarker, false);
 479:     document.getElementById('fit').addEventListener('click', fitting, false);
 480: 
 481:     /**
 482:      * マーカーのドラッグエンド処理
 483:      * @param   なし
 484:      * @return  なし
 485:     */
 486:     function dragendMarker() {
 487:         let pcount = Number(document.getElementById('pcount').value);
 488: 
 489:         //大圏航法軌跡の再描画と距離の再計算
 490:         let m0, m1;
 491:         for (let i = 1; i < pcount; i++) {
 492:             m0 = Dmarkers[i - 1].getPosition();
 493:             m1 = Dmarkers[i].getPosition();
 494:             Distances[i] = greatCircleDistance(m0.lng(), m0.lat(), m1.lng(), m1.lat());
 495:             let points = greatCircleSailing(m0.lng(), m0.lat(), m1.lng(), m1.lat());
 496:             let line = [];
 497:             for (let j = 0; j < points.length; j++) {
 498:                 line[j] = new google.maps.LatLng(points[j].latitude, points[j].longitude);
 499:             }
 500:             //古い軌跡を削除
 501:             Dlines[i].setMap(null);
 502:             //新しい軌跡を描く
 503:             Dlines[i] = new google.maps.Polyline({
 504:                 map: map,
 505:                 path: line,
 506:                 strokeColor: '{$color}',
 507:                 strokeOpacity: 1,
 508:                 strokeWeight: {$weight}
 509:             });
 510:             stringifyMarkers();
 511:         }
 512:         writeDtable(Distances);         //距離TABLE更新
 513:     }
 514: 
 515:     /**
 516:      * 軌跡の最後にマーカーを1つ追加する
 517:      * @param   LatLng point 追加する座標
 518:      * @return  なし
 519:     */
 520:     function addMarker(e) {
 521:         let lat = e.latLng.lat();
 522:         let lng = e.latLng.lng();
 523:         _addMarker(lat, lng);
 524:     }
 525: 
 526:     /**
 527:      * 軌跡の最後にマーカーを1つ追加する(下請け)
 528:      * @param   float lat 緯度
 529:      * @param   float lng 経度
 530:      * @return  なし
 531:     */
 532:     function _addMarker(lat, lng) {
 533:         let dd;
 534:         let pcount = Number(document.getElementById('pcount').value);
 535:         if (pcount >= {$maxMarkers})    return;     //マーカー数の上限チェック
 536:         let m0, m1;
 537: 
 538:         //マーカー追加
 539:         Dmarkers[pcount] = new google.maps.Marker({
 540:             map: map,
 541:             position: new google.maps.LatLng(lat, lng),
 542:             icon: {
 543:                 fillColor: '#FF4500',
 544:                 fillOpacity: 0.8,
 545:                 path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW,
 546:                 scale: 4,
 547:                 strokeColor: "#FF4500",
 548:                 strokeWeight: 1.0
 549:             }
 550:         });
 551: 
 552:         //ドラッグエンド・イベント処理
 553:         Dmarkers[pcount].setDraggable(true);
 554:         Dmarkers[pcount].addListener('dragend', dragendMarker);
 555: 
 556:         //大圏航法軌跡の描画と距離の再計算
 557:         if (pcount >= 1) {
 558:             m0 = Dmarkers[pcount - 1].getPosition();
 559:             m1 = Dmarkers[pcount - 0].getPosition();
 560:             let points = greatCircleSailing(m0.lng(), m0.lat(), m1.lng(), m1.lat());
 561:             let line = [];
 562:             for (let j = 0; j < points.length; j++) {
 563:                 line[j] = new google.maps.LatLng(points[j].latitude, points[j].longitude);
 564:             }
 565:             Dlines[pcount] = new google.maps.Polyline({
 566:                 map: map,
 567:                 path: line,
 568:                 strokeColor: '{$color}',
 569:                 strokeOpacity: 1,
 570:                 strokeWeight: {$weight}
 571:             });
 572:             Distances[pcount] = greatCircleDistance(m0.lng(), m0.lat(), m1.lng(), m1.lat());
 573:         }
 574:         document.getElementById('pcount').value = pcount + 1;
 575:         writeDtable(Distances);     //距離TABLE更新
 576:         stringifyMarkers();         //マーカー座標JSON作成
 577:     }
 578: 
 579:     /**
 580:      * 軌跡の最後のマーカーを1つ削除する
 581:      * @param   なし
 582:      * @return  なし
 583:     */
 584:     function delMarker() {
 585:         let pcount = Number(document.getElementById('pcount').value);
 586:         if (pcount > 0)     pcount--;
 587: 
 588:         //最後の地点間距離を削除
 589:         Distances.pop();
 590: 
 591:         //マーカーと軌跡の削除
 592:         Dmarkers[pcount].setMap(null);
 593:         Dlines[pcount].setMap(null);
 594:         stringifyMarkers();         //マーカー座標JSON作成
 595:         document.getElementById('pcount').value = pcount;
 596:         writeDtable(Distances);     //距離TABLE更新
 597:     }
 598: 
 599:     /**
 600:      * マーカーの位置情報をJSON文字列に変換して代入
 601:      * @param   なし
 602:      * @return  なし
 603:     */
 604:     function stringifyMarkers() {
 605:         let points = [];
 606:         let pcount = Number(document.getElementById('pcount').value);
 607:         for (let i = 0; i < pcount; i++) {
 608:             points[i] = {
 609:                 'latitude'  : Dmarkers[i].getPosition().lat(),
 610:                 'longitude' : Dmarkers[i].getPosition().lng()
 611:             };
 612:         }
 613:         document.getElementById('points').value = JSON.stringify(points);
 614:     }
 615: 
 616:     /**
 617:      * 地図を最適サイズにフィットする
 618:      * @param   なし
 619:      * @return  なし
 620:     */
 621:     function fitting() {
 622:         let lat, lng, lat0, lng0, lat1, lng1;
 623:         let pcount = Number(document.getElementById('pcount').value);
 624:         //南西端(lat0, lng0), 北東端(lat1, lng1)を求める
 625:         for (let i = 0; i < pcount; i++) {
 626:             lat = Dmarkers[i].getPosition().lat();
 627:             lng = Dmarkers[i].getPosition().lng();
 628:             if (i == 0) {
 629:                 lat0 = lat1 = lat;
 630:                 lng0 = lng1 = lng;
 631:             }
 632:             if (lat0 > lat)     lat0 = lat;
 633:             if (lng0 > lng)     lng0 = lng;
 634:             if (lat1 < lat)     lat1 = lat;
 635:             if (lng1 < lng)     lng1 = lng;
 636:         }
 637:         let bounds = new google.maps.LatLngBounds(new google.maps.LatLng(lat0, lng0), new google.maps.LatLng(lat1, lng1));
 638:         map.fitBounds(bounds);                      //地図表示の最適化
 639:     }
 640: 
 641: EOT;
 642:     return $js;
 643: }

地図に対するインタラクティブな処理は、Google JavaScriptマップLeafletで異なるため、JavaScriptコードを生成するPHP関数を分けて用意した。

冒頭では、複数のマーカー情報を格納する配列変数 Dmarkers、マーカー間の軌跡を格納する配列変数 Dlines、マーカー間の距離を格納する配列変数 Distances、マップ・クリック時のイベントハンドラを格納する版数 EventListner を、それぞれグローバル変数として用意する。
サーバサイドに処理を渡す際、マーカーの位置情報はJSONテキストに書き出して保存している。このJSONを読み込み、個々のマーカー座標に分離し、後述するマーカー追加関数 _addMarker に渡すことで、初期状態のマーカーと軌跡を描く。

マップをクリックした時のイベントは、後述するマーカー追加関数 addMarker に紐付ける。
「1つ戻る」ボタンをクリックした時のイベントは、後述するマーカー削除関数 delMarker に紐付ける。
「フィット」ボタンをクリックした時のイベントは、後述する地図の最適表示関数 fittng に紐付ける。

 515:     /**
 516:      * 軌跡の最後にマーカーを1つ追加する
 517:      * @param   LatLng point 追加する座標
 518:      * @return  なし
 519:     */
 520:     function addMarker(e) {
 521:         let lat = e.latLng.lat();
 522:         let lng = e.latLng.lng();
 523:         _addMarker(lat, lng);
 524:     }

ユーザー関数 addMarker は、マップをクリックした時、軌跡の最後にマーカーを1つ追加するもので、クリックした位置にマーカーを設置する。同時に、直前のマーカーから大圏航路軌跡を連続して描く。異なる引数で呼び出しをする都合上、処理の実体は、下請け関数 _addMarker にある。

現在のマーカー数は、HTMLのFORMテキスト pcount に格納してある。
まず、この値を参照し、最大数を超えていないかどうかをチェックする。

続いてマーカーを追加する。ここでは icon オブジェクトに矢印形のアイコンを定義しているが、形状は自由に変更して構わない。
マーカーはドラッグできるようにして、ドラッグエンドの時の処理を紐付けておく。

次に、直前のマーカーとの間に大圏航路軌跡を描き、その距離を計算する。
最後にマーカー数をアップカウントし、距離TABLEの更新とマーカー座標JSONの再作成を行っておく。

 481:     /**
 482:      * マーカーのドラッグエンド処理
 483:      * @param   なし
 484:      * @return  なし
 485:     */
 486:     function dragendMarker() {
 487:         let pcount = Number(document.getElementById('pcount').value);
 488: 
 489:         //大圏航法軌跡の再描画と距離の再計算
 490:         let m0, m1;
 491:         for (let i = 1; i < pcount; i++) {
 492:             m0 = Dmarkers[i - 1].getPosition();
 493:             m1 = Dmarkers[i].getPosition();
 494:             Distances[i] = greatCircleDistance(m0.lng(), m0.lat(), m1.lng(), m1.lat());
 495:             let points = greatCircleSailing(m0.lng(), m0.lat(), m1.lng(), m1.lat());
 496:             let line = [];
 497:             for (let j = 0; j < points.length; j++) {
 498:                 line[j] = new google.maps.LatLng(points[j].latitude, points[j].longitude);
 499:             }
 500:             //古い軌跡を削除
 501:             Dlines[i].setMap(null);
 502:             //新しい軌跡を描く
 503:             Dlines[i] = new google.maps.Polyline({
 504:                 map: map,
 505:                 path: line,
 506:                 strokeColor: '{$color}',
 507:                 strokeOpacity: 1,
 508:                 strokeWeight: {$weight}
 509:             });
 510:             stringifyMarkers();
 511:         }
 512:         writeDtable(Distances);         //距離TABLE更新
 513:     }

ユーザー関数 dragendMarker は、マーカーをドラッグエンドしたときに呼び出される。すべてのマーカーについて距離を再計算し、マーカー間の軌跡を再描画する。

 579:     /**
 580:      * 軌跡の最後のマーカーを1つ削除する
 581:      * @param   なし
 582:      * @return  なし
 583:     */
 584:     function delMarker() {
 585:         let pcount = Number(document.getElementById('pcount').value);
 586:         if (pcount > 0)     pcount--;
 587: 
 588:         //最後の地点間距離を削除
 589:         Distances.pop();
 590: 
 591:         //マーカーと軌跡の削除
 592:         Dmarkers[pcount].setMap(null);
 593:         Dlines[pcount].setMap(null);
 594:         stringifyMarkers();         //マーカー座標JSON作成
 595:         document.getElementById('pcount').value = pcount;
 596:         writeDtable(Distances);     //距離TABLE更新
 597:     }

ユーザー関数 delMarker は、「1つ戻る」ボタンをクリックした時に呼び出される。軌跡の最後のマーカーと、軌跡を削除する。
あわせて、関連する配列変数の最後の要素を削除し、マーカー数をダウンカウントと、距離TABLEの更新とマーカー座標JSONの再作成を行う。

 599:     /**
 600:      * マーカーの位置情報をJSON文字列に変換して代入
 601:      * @param   なし
 602:      * @return  なし
 603:     */
 604:     function stringifyMarkers() {
 605:         let points = [];
 606:         let pcount = Number(document.getElementById('pcount').value);
 607:         for (let i = 0; i < pcount; i++) {
 608:             points[i] = {
 609:                 'latitude'  : Dmarkers[i].getPosition().lat(),
 610:                 'longitude' : Dmarkers[i].getPosition().lng()
 611:             };
 612:         }
 613:         document.getElementById('points').value = JSON.stringify(points);
 614:     }

ユーザー関数 stringifyMarkers は、マーカー位置を保存しているグローバル配列変数 Dmarkers をJSONテキストに変換し、FORMテキスト points に代入する。
処理がサーバサイドに移った時、この情報をPOST渡しすることでマーカー座標が失われないようにする。

 616:     /**
 617:      * 地図を最適サイズにフィットする
 618:      * @param   なし
 619:      * @return  なし
 620:     */
 621:     function fitting() {
 622:         let lat, lng, lat0, lng0, lat1, lng1;
 623:         let pcount = Number(document.getElementById('pcount').value);
 624:         //南西端(lat0, lng0), 北東端(lat1, lng1)を求める
 625:         for (let i = 0; i < pcount; i++) {
 626:             lat = Dmarkers[i].getPosition().lat();
 627:             lng = Dmarkers[i].getPosition().lng();
 628:             if (i == 0) {
 629:                 lat0 = lat1 = lat;
 630:                 lng0 = lng1 = lng;
 631:             }
 632:             if (lat0 > lat)     lat0 = lat;
 633:             if (lng0 > lng)     lng0 = lng;
 634:             if (lat1 < lat)     lat1 = lat;
 635:             if (lng1 < lng)     lng1 = lng;
 636:         }
 637:         let bounds = new google.maps.LatLngBounds(new google.maps.LatLng(lat0, lng0), new google.maps.LatLng(lat1, lng1));
 638:         map.fitBounds(bounds);                      //地図表示の最適化
 639:     }

ユーザー関数 fitting は、「フィット」ボタンをクリックした時に呼び出される。すべてのマーカーが地図上に収まるように、ズームと中心点を最適化する。
変数 Dmarkers をスキャンして、マーカー群の南西端と北東端を求め、これをGooleマップAPIの fitBounds メソッドに渡すことで実現している。
後述する経路探索では、経路の南西端と北東端が求めにくいため、経路探索中にフィット機能は使用できないようにしている。

解説:Google JavaScriptマップ用スクリプト(経路探索)

 645: /**
 646:  * Google JavaScriptマップ用スクリプトを(経路探索)を生成する.
 647:  * @param   なし
 648:  * @return  string JavaScript
 649: */
 650: function jsGroute() {
 651:     $route_color  = '#' . ROUTE_COLOR;
 652:     $route_weight = ROUTE_WEIGHT;
 653: 
 654:     $js =<<< EOT
 655:     let LineRoute;                      //経路ライン
 656: 
 657:     //イベント設定
 658:     document.getElementById('route').addEventListener('click', drawRoute, false);
 659:     document.getElementById('redraw').addEventListener('click', delRoute, false);
 660:     setButtons(['redraw'], 'disable');
 661: 
 662:     /**
 663:      * 経路探索し,軌跡を消去
 664:      * @param   なし
 665:      * @return  なし
 666:      * 参考 https://qiita.com/nissuk/items/7e0225ae3764d9f1bef7
 667:     */
 668:     function drawRoute() {
 669:         //クリック・イベント削除
 670:         google.maps.event.removeListener(EventListner);
 671:         setButtons(['exec', 'delmarker', 'fit', 'route'], 'disable');
 672:         setButtons(['redraw'], 'enable');
 673: 
 674:         let pcount = Number(document.getElementById('pcount').value);
 675:         if (pcount < 2)     return;
 676: 
 677:         latlngs = [];
 678:         for (let i = 0; i < pcount; i++) {
 679:             latlngs[i] = Dmarkers[i].getPosition();
 680:             if (i > 0)  Dlines[i].setMap(null);         //軌跡を消去
 681:         }
 682:         //経由地点8カ所以上は分割して経路探索
 683:         let d = new google.maps.DirectionsService();    //経路探索オブジェクト
 684:         let origin = null;      //出発地
 685:         let waypoints = [];     //経由地
 686:         let dest = null;        //目的地
 687:         let resultMap = {};     //分割経路探索の結果
 688:         let requestIndex = 0;   //分割検索番号
 689:         let done = 0;           //検索完了数
 690:         for (let i = 0, len = latlngs.length; i < len; i++) {
 691:             //検索の最初
 692:             if (origin == null) {
 693:                 origin = latlngs[i];
 694:             //経由地が8カ所になった or 目的地→経路探索
 695:             } else if (waypoints.length == 8 || i == len - 1) {
 696:                 dest = latlngs[i];
 697:                 (function(index) {
 698:                     //検索条件
 699:                     let request = {
 700:                         origin: origin,         //出発地
 701:                         waypoints: waypoints,   //経由地
 702:                         destination: dest,      //目的地
 703:                         travelMode: google.maps.DirectionsTravelMode.DRIVING
 704:                                                 //移動手段:自動車
 705:                     };
 706:                     //経路探索実行
 707:                     d.route(request, function(result, status) {
 708:                         //経路データ保持
 709:                         if (status == google.maps.DirectionsStatus.OK) {
 710:                             resultMap[index] = result;
 711:                             done++;
 712:                         } else {
 713:                             console.log(status);        //デバッグ用
 714:                         }
 715:                     });
 716:                 })(requestIndex);
 717: 
 718:                 requestIndex++;
 719:                 origin = latlngs[i];        //目的地を次の出発地へ
 720:                 waypoints = [];
 721:             //それ以外→waypointsに地点を追加
 722:             } else {
 723:                 waypoints.push({ location: latlngs[i], stopover: true });
 724:             }
 725:         }
 726:         //経路を配列へ
 727:         let sid = setInterval(function() {
 728:             //分割したすべての検索が完了するまで待つ
 729:             if (requestIndex > done)    return;
 730:             clearInterval(sid);
 731: 
 732:             //すべての経由座標を順番に取得して平坦な配列に置換
 733:             let path = [];
 734:             let result;
 735:             let o = { 'TotalDistance' : 0, 'TotalTime' : 0 };
 736:             for (let i = 0, len = requestIndex; i < len; i++) {
 737:                 result = resultMap[i];      //検索結果
 738:                 let legs = result.routes[0].legs;       //Array<DirectionsLeg>
 739:                 for (let li = 0, llen = legs.length; li < llen; li++) {
 740:                     let leg = legs[li];                 //DirectionLeg
 741:                     o.TotalDistance += leg.distance.value;      //経由距離
 742:                     o.TotalTime += (leg.duration.value / 60);   //経由時間
 743:                     let steps = leg.steps;              //Array<DirectionsStep>
 744:                     //DirectionsStepが持っているpathを取得して平坦(2次元配列→1次元配列)に
 745:                     let _path = steps.map(function(step){ return step.path })
 746:                         .reduce(function(all, paths){ return all.concat(paths) });
 747:                     path = path.concat(_path);
 748:                 }
 749:             }
 750:             //経路描画
 751:             LineRoute = new google.maps.Polyline({
 752:                 map: map,
 753:                 strokeColor: '{$route_color}',
 754:                 strokeOpacity: 0.8,
 755:                 strokeWeight: {$route_weight},
 756:                 path: path
 757:             });
 758:             //小数点以下を丸める
 759:             o.TotalDistance = Math.round(o.TotalDistance);
 760:             o.TotalTime = Math.round(o.TotalTime);
 761:             writeRtable(o);
 762:         }, 1000);
 763:     }
 764: 
 765:     /**
 766:      * 経路を消去し,軌跡を再描画
 767:      * @param   なし
 768:      * @return  なし
 769:     */
 770:     function delRoute() {
 771:         //経路消去
 772:         LineRoute.setMap(null);     //経路消去
 773:         //軌跡の再描画
 774:         let pcount = Number(document.getElementById('pcount').value);
 775:         for (let i = 1; i < pcount; i++) {
 776:             Dlines[i].setMap(map);
 777:         };
 778:         //経路TABLE初期化
 779:         initRtable();
 780:         //クリック・イベント再開
 781:         EventListner = google.maps.event.addListener(map, 'click', addMarker);
 782:         setButtons(['exec', 'delmarker', 'fit', 'route'], 'enable');
 783:         setButtons(['redraw'], 'disable');
 784:     }
 785: 
 786: EOT;
 787:     return $js;
 788: }

Google JavaScriptマップでの経路探索については、@nissuk さんの記事「Google Maps API: たくさんの経由地点を含めてルート検索する」(Qiita)を参考にした。

 662:     /**
 663:      * 経路探索し,軌跡を消去
 664:      * @param   なし
 665:      * @return  なし
 666:      * 参考 https://qiita.com/nissuk/items/7e0225ae3764d9f1bef7
 667:     */
 668:     function drawRoute() {
 669:         //クリック・イベント削除
 670:         google.maps.event.removeListener(EventListner);
 671:         setButtons(['exec', 'delmarker', 'fit', 'route'], 'disable');
 672:         setButtons(['redraw'], 'enable');
 673: 
 674:         let pcount = Number(document.getElementById('pcount').value);
 675:         if (pcount < 2)     return;
 676: 
 677:         latlngs = [];
 678:         for (let i = 0; i < pcount; i++) {
 679:             latlngs[i] = Dmarkers[i].getPosition();
 680:             if (i > 0)  Dlines[i].setMap(null);         //軌跡を消去
 681:         }
 682:         //経由地点8カ所以上は分割して経路探索
 683:         let d = new google.maps.DirectionsService();    //経路探索オブジェクト
 684:         let origin = null;      //出発地
 685:         let waypoints = [];     //経由地
 686:         let dest = null;        //目的地
 687:         let resultMap = {};     //分割経路探索の結果
 688:         let requestIndex = 0;   //分割検索番号
 689:         let done = 0;           //検索完了数
 690:         for (let i = 0, len = latlngs.length; i < len; i++) {
 691:             //検索の最初
 692:             if (origin == null) {
 693:                 origin = latlngs[i];
 694:             //経由地が8カ所になった or 目的地→経路探索
 695:             } else if (waypoints.length == 8 || i == len - 1) {
 696:                 dest = latlngs[i];
 697:                 (function(index) {
 698:                     //検索条件
 699:                     let request = {
 700:                         origin: origin,         //出発地
 701:                         waypoints: waypoints,   //経由地
 702:                         destination: dest,      //目的地
 703:                         travelMode: google.maps.DirectionsTravelMode.DRIVING
 704:                                                 //移動手段:自動車
 705:                     };
 706:                     //経路探索実行
 707:                     d.route(request, function(result, status) {
 708:                         //経路データ保持
 709:                         if (status == google.maps.DirectionsStatus.OK) {
 710:                             resultMap[index] = result;
 711:                             done++;
 712:                         } else {
 713:                             console.log(status);        //デバッグ用
 714:                         }
 715:                     });
 716:                 })(requestIndex);
 717: 
 718:                 requestIndex++;
 719:                 origin = latlngs[i];        //目的地を次の出発地へ
 720:                 waypoints = [];
 721:             //それ以外→waypointsに地点を追加
 722:             } else {
 723:                 waypoints.push({ location: latlngs[i], stopover: true });
 724:             }
 725:         }
 726:         //経路を配列へ
 727:         let sid = setInterval(function() {
 728:             //分割したすべての検索が完了するまで待つ
 729:             if (requestIndex > done)    return;
 730:             clearInterval(sid);
 731: 
 732:             //すべての経由座標を順番に取得して平坦な配列に置換
 733:             let path = [];
 734:             let result;
 735:             let o = { 'TotalDistance' : 0, 'TotalTime' : 0 };
 736:             for (let i = 0, len = requestIndex; i < len; i++) {
 737:                 result = resultMap[i];      //検索結果
 738:                 let legs = result.routes[0].legs;       //Array<DirectionsLeg>
 739:                 for (let li = 0, llen = legs.length; li < llen; li++) {
 740:                     let leg = legs[li];                 //DirectionLeg
 741:                     o.TotalDistance += leg.distance.value;      //経由距離
 742:                     o.TotalTime += (leg.duration.value / 60);   //経由時間
 743:                     let steps = leg.steps;              //Array<DirectionsStep>
 744:                     //DirectionsStepが持っているpathを取得して平坦(2次元配列→1次元配列)に
 745:                     let _path = steps.map(function(step){ return step.path })
 746:                         .reduce(function(all, paths){ return all.concat(paths) });
 747:                     path = path.concat(_path);
 748:                 }
 749:             }
 750:             //経路描画
 751:             LineRoute = new google.maps.Polyline({
 752:                 map: map,
 753:                 strokeColor: '{$route_color}',
 754:                 strokeOpacity: 0.8,
 755:                 strokeWeight: {$route_weight},
 756:                 path: path
 757:             });
 758:             //小数点以下を丸める
 759:             o.TotalDistance = Math.round(o.TotalDistance);
 760:             o.TotalTime = Math.round(o.TotalTime);
 761:             writeRtable(o);
 762:         }, 1000);
 763:     }

ユーザー関数 drawRoute は、マーカーが3つ以上の場合は途中のマーカーを経由地点と見なして経路探索を行う。ただし、経由地点が8以上はAPIの制約に引っかかるため、8箇所に分割して経路探索を行う。
経路は変数legsに入ってくるので、これをマップ上に描画するとともに距離積算、経由時間積算を行う。

 765:     /**
 766:      * 経路を消去し,軌跡を再描画
 767:      * @param   なし
 768:      * @return  なし
 769:     */
 770:     function delRoute() {
 771:         //経路消去
 772:         LineRoute.setMap(null);     //経路消去
 773:         //軌跡の再描画
 774:         let pcount = Number(document.getElementById('pcount').value);
 775:         for (let i = 1; i < pcount; i++) {
 776:             Dlines[i].setMap(map);
 777:         };
 778:         //経路TABLE初期化
 779:         initRtable();
 780:         //クリック・イベント再開
 781:         EventListner = google.maps.event.addListener(map, 'click', addMarker);
 782:         setButtons(['exec', 'delmarker', 'fit', 'route'], 'enable');
 783:         setButtons(['redraw'], 'disable');
 784:     }

ユーザー関数 delRoute は、経路を消去し、軌跡を再描画する。

解説:Leaflet用スクリプト(マーカー・軌跡)

1037: /**
1038:  * Leafletマップ用スクリプト(マーカー・軌跡)を生成する.
1039:  * @param   string $points マーカーの初期座標(JSONテキスト)
1040:  * @return  string JavaScript
1041: */
1042: function jsLeaflet($points) {
1043:     $maxMarkers = MAX_MARKERS;          //マーカーの最大数
1044:     $color  = '#' . LINE_COLOR;
1045:     $weight = LINE_WEIGHT;
1046: 
1047:     $js =<<< EOT
1048:     let Dmarkers  = Array();            //マーカー格納用
1049:     let Dlines    = Array();            //軌跡格納用
1050:     let Distances = Array();            //距離格納用
1051: 
1052:     //マーカーの初期状態
1053:     let points = JSON.parse('{$points}');
1054:     document.getElementById('pcount').value = 0;
1055:     for (let i = 0; i < points.length; i++) {
1056:         _addMarker(Array(points[i].latitude, points[i].longitude));
1057:     }
1058: 
1059:     //イベント設定
1060:     map.on('click', addMarker);
1061:     document.getElementById('delmarker').addEventListener('click', delMarker, false);
1062:     document.getElementById('fit').addEventListener('click', fitting, false);
1063: 
1064:     /**
1065:      * マーカーのドラッグエンド処理
1066:      * @param   なし
1067:      * @return  なし
1068:     */
1069:     function dragendMarker() {
1070:         let pcount = Number(document.getElementById('pcount').value);
1071: 
1072:         //大圏航法軌跡の再描画と距離の再計算
1073:         let m0, m1;
1074:         for (let i = 1; i < pcount; i++) {
1075:             m0 = Dmarkers[i - 1].getLatLng();
1076:             m1 = Dmarkers[i].getLatLng();
1077:             Distances[i] = greatCircleDistance(m0.lng, m0.lat, m1.lng, m1.lat);
1078:             let points = greatCircleSailing(m0.lng, m0.lat, m1.lng, m1.lat);
1079:             let line = [];
1080:             for (let j = 0; j < points.length; j++) {
1081:                 line[j] = Array(points[j].latitude, points[j].longitude);
1082:             }
1083:             //古い軌跡を削除
1084:             map.removeLayer(Dlines[i]);
1085:             Dlines[i] = null;
1086:             //新しい軌跡を描く
1087:             Dlines[i] = L.polyline(
1088:                 line, {
1089:                 'color': '{$color}',
1090:                 'opacity': 1,
1091:                 'weight': {$weight}
1092:             }).addTo(map);
1093:             stringifyMarkers();
1094:         }
1095:         writeDtable(Distances);         //距離TABLE更新
1096:     }
1097: 
1098:     /**
1099:      * 軌跡の最後にマーカーを1つ追加する
1100:      * @param   LatLng point 追加する座標
1101:      * @return  なし
1102:     */
1103:     function addMarker(e) {
1104:         _addMarker(e.latlng);
1105:     }
1106: 
1107:     /**
1108:      * 軌跡の最後にマーカーを1つ追加する(下請け)
1109:      * @param   float lat 緯度
1110:      * @param   float lng 経度
1111:      * @return  なし
1112:     */
1113:     function _addMarker(latlng) {
1114:         let pcount = Number(document.getElementById('pcount').value);
1115:         if (pcount >= {$maxMarkers})    return;     //マーカー数の上限チェック
1116: 
1117:         //マーカー追加
1118:         Dmarkers[pcount] = new L.marker(
1119:             latlng, {
1120:             draggable: true,
1121:         }).addTo(map).on('dragend', dragendMarker);
1122: 
1123:         //大圏航法軌跡の描画と距離の再計算
1124:         if (pcount >= 1) {
1125:             let m0 = Dmarkers[pcount - 1].getLatLng();
1126:             let m1 = Dmarkers[pcount - 0].getLatLng();
1127:             let points = greatCircleSailing(m0.lng, m0.lat, m1.lng, m1.lat);
1128:             let line = [];
1129:             for (let j = 0; j < points.length; j++) {
1130:                 line[j] = Array(points[j].latitude, points[j].longitude);
1131:             }
1132:             Dlines[pcount] = L.polyline(
1133:                 line, {
1134:                 'color': '{$color}',
1135:                 'opacity': 1,
1136:                 'weight': {$weight}
1137:             }).addTo(map);
1138:             Distances[pcount] = greatCircleDistance(m0.lng, m0.lat, m1.lng, m1.lat);
1139:         }
1140:         document.getElementById('pcount').value = pcount + 1;
1141:         writeDtable(Distances);     //距離TABLE更新
1142:         stringifyMarkers();         //マーカー座標JSON作成
1143:     }
1144: 
1145:     /**
1146:      * 軌跡の最後のマーカーを1つ削除する
1147:      * @param   なし
1148:      * @return  なし
1149:     */
1150:     function delMarker() {
1151:         let pcount = Number(document.getElementById('pcount').value);
1152:         if (pcount > 0)     pcount--;
1153:         Dmarkers[pcount].dragging.disable();        //ドラッグ禁止
1154: 
1155:         //最後の地点距離を削除
1156:         Distances.pop();
1157: 
1158:         //マーカーと軌跡の削除
1159:         map.removeLayer(Dmarkers[pcount]);
1160:         map.removeLayer(Dlines[pcount]);
1161:         stringifyMarkers();
1162:         document.getElementById('pcount').value = pcount;
1163:         writeDtable(Distances);     //距離TABLE更新
1164:     }
1165: 
1166:     /**
1167:      * マーカーの位置情報をJSON文字列に変換して代入
1168:      * @param   なし
1169:      * @return  なし
1170:     */
1171:     function stringifyMarkers() {
1172:         let points = [];
1173:         let pcount = Number(document.getElementById('pcount').value);
1174:         for (let i = 0; i < pcount; i++) {
1175:             points[i] = {
1176:                 'latitude'  : Dmarkers[i].getLatLng().lat,
1177:                 'longitude' : Dmarkers[i].getLatLng().lng
1178:             };
1179:         }
1180:         document.getElementById('points').value = JSON.stringify(points);
1181:     }
1182: 
1183:     /**
1184:      * 地図を最適サイズにフィットする
1185:      * @param   なし
1186:      * @return  なし
1187:     */
1188:     function fitting() {
1189:         let lat0, lng0, lat1, lng1;
1190:         let pcount = Number(document.getElementById('pcount').value);
1191:         //南西端(lat0, lng0), 北東端(lat1, lng1)を求める
1192:         for (let i = 0; i < pcount; i++) {
1193:             points[i] = {
1194:                 'latitude'  : Dmarkers[i].getLatLng().lat,
1195:                 'longitude' : Dmarkers[i].getLatLng().lng
1196:             };
1197:             if (i == 0) {
1198:                 lat0 = lat1 = points[i].latitude;
1199:                 lng0 = lng1 = points[i].longitude;
1200:             }
1201:             if (lat0 > points[i].latitude)  lat0 = points[i].latitude;
1202:             if (lng0 > points[i].longitude) lng0 = points[i].longitude;
1203:             if (lat1 < points[i].latitude)  lat1 = points[i].latitude;
1204:             if (lng1 < points[i].longitude) lng1 = points[i].longitude;
1205:         }
1206:         map.fitBounds([[lat0, lng0], [lat1, lng1]]);
1207:     }
1208: 
1209: EOT;
1210:     return $js;
1211: }

地図に対するインタラクティブな処理は、Google JavaScriptマップ用の関数 jsGmap と流れは同じで、異なるイベントやパラメータを書き直している。
なお、Leafletでは経路探索はできない。オープンストリートマップ側でフリーの経路探索エンジンが用意されているという情報もあったが、複数のサービスをあたってみたが、動作していなかったり、日本国内ではうまく動作しないようなので実装を見送った。

1098:     /**
1099:      * 軌跡の最後にマーカーを1つ追加する
1100:      * @param   LatLng point 追加する座標
1101:      * @return  なし
1102:     */
1103:     function addMarker(e) {
1104:         _addMarker(e.latlng);
1105:     }
1106: 
1107:     /**
1108:      * 軌跡の最後にマーカーを1つ追加する(下請け)
1109:      * @param   float lat 緯度
1110:      * @param   float lng 経度
1111:      * @return  なし
1112:     */
1113:     function _addMarker(latlng) {
1114:         let pcount = Number(document.getElementById('pcount').value);
1115:         if (pcount >= {$maxMarkers})    return;     //マーカー数の上限チェック
1116: 
1117:         //マーカー追加
1118:         Dmarkers[pcount] = new L.marker(
1119:             latlng, {
1120:             draggable: true,
1121:         }).addTo(map).on('dragend', dragendMarker);
1122: 
1123:         //大圏航法軌跡の描画と距離の再計算
1124:         if (pcount >= 1) {
1125:             let m0 = Dmarkers[pcount - 1].getLatLng();
1126:             let m1 = Dmarkers[pcount - 0].getLatLng();
1127:             let points = greatCircleSailing(m0.lng, m0.lat, m1.lng, m1.lat);
1128:             let line = [];
1129:             for (let j = 0; j < points.length; j++) {
1130:                 line[j] = Array(points[j].latitude, points[j].longitude);
1131:             }
1132:             Dlines[pcount] = L.polyline(
1133:                 line, {
1134:                 'color': '{$color}',
1135:                 'opacity': 1,
1136:                 'weight': {$weight}
1137:             }).addTo(map);
1138:             Distances[pcount] = greatCircleDistance(m0.lng, m0.lat, m1.lng, m1.lat);
1139:         }
1140:         document.getElementById('pcount').value = pcount + 1;
1141:         writeDtable(Distances);     //距離TABLE更新
1142:         stringifyMarkers();         //マーカー座標JSON作成
1143:     }

ユーザー関数 addMarker, _addMarker は、Google JavaScriptマップ用とほぼ同じだが、パラメータとしての緯度・経度の記述方法が異なる。

1064:     /**
1065:      * マーカーのドラッグエンド処理
1066:      * @param   なし
1067:      * @return  なし
1068:     */
1069:     function dragendMarker() {
1070:         let pcount = Number(document.getElementById('pcount').value);
1071: 
1072:         //大圏航法軌跡の再描画と距離の再計算
1073:         let m0, m1;
1074:         for (let i = 1; i < pcount; i++) {
1075:             m0 = Dmarkers[i - 1].getLatLng();
1076:             m1 = Dmarkers[i].getLatLng();
1077:             Distances[i] = greatCircleDistance(m0.lng, m0.lat, m1.lng, m1.lat);
1078:             let points = greatCircleSailing(m0.lng, m0.lat, m1.lng, m1.lat);
1079:             let line = [];
1080:             for (let j = 0; j < points.length; j++) {
1081:                 line[j] = Array(points[j].latitude, points[j].longitude);
1082:             }
1083:             //古い軌跡を削除
1084:             map.removeLayer(Dlines[i]);
1085:             Dlines[i] = null;
1086:             //新しい軌跡を描く
1087:             Dlines[i] = L.polyline(
1088:                 line, {
1089:                 'color': '{$color}',
1090:                 'opacity': 1,
1091:                 'weight': {$weight}
1092:             }).addTo(map);
1093:             stringifyMarkers();
1094:         }
1095:         writeDtable(Distances);         //距離TABLE更新
1096:     }

ユーザー関数 dragendMarker は、マーカーをドラッグエンドしたときに呼び出される。すべてのマーカーについて距離を再計算し、マーカー間の軌跡を再描画する。

1145:     /**
1146:      * 軌跡の最後のマーカーを1つ削除する
1147:      * @param   なし
1148:      * @return  なし
1149:     */
1150:     function delMarker() {
1151:         let pcount = Number(document.getElementById('pcount').value);
1152:         if (pcount > 0)     pcount--;
1153:         Dmarkers[pcount].dragging.disable();        //ドラッグ禁止
1154: 
1155:         //最後の地点距離を削除
1156:         Distances.pop();
1157: 
1158:         //マーカーと軌跡の削除
1159:         map.removeLayer(Dmarkers[pcount]);
1160:         map.removeLayer(Dlines[pcount]);
1161:         stringifyMarkers();
1162:         document.getElementById('pcount').value = pcount;
1163:         writeDtable(Distances);     //距離TABLE更新
1164:     }

ユーザー関数 delMarker は、「1つ戻る」ボタンをクリックした時に呼び出される。軌跡の最後のマーカーと、軌跡を削除する。
あわせて、関連する配列変数の最後の要素を削除し、マーカー数をダウンカウントと、距離TABLEの更新とマーカー座標JSONの再作成を行う。

1167:      * マーカーの位置情報をJSON文字列に変換して代入
1168:      * @param   なし
1169:      * @return  なし
1170:     */
1171:     function stringifyMarkers() {
1172:         let points = [];
1173:         let pcount = Number(document.getElementById('pcount').value);
1174:         for (let i = 0; i < pcount; i++) {
1175:             points[i] = {
1176:                 'latitude'  : Dmarkers[i].getLatLng().lat,
1177:                 'longitude' : Dmarkers[i].getLatLng().lng
1178:             };
1179:         }
1180:         document.getElementById('points').value = JSON.stringify(points);
1181:     }

ユーザー関数 stringifyMarkers は、マーカー位置を保存しているグローバル配列変数 Dmarkers をJSONテキストに変換し、FORMテキスト points に代入する。
処理がサーバサイドに移った時、この情報をPOST渡しすることでマーカー座標が失われないようにする。

1183:     /**
1184:      * 地図を最適サイズにフィットする
1185:      * @param   なし
1186:      * @return  なし
1187:     */
1188:     function fitting() {
1189:         let lat0, lng0, lat1, lng1;
1190:         let pcount = Number(document.getElementById('pcount').value);
1191:         //南西端(lat0, lng0), 北東端(lat1, lng1)を求める
1192:         for (let i = 0; i < pcount; i++) {
1193:             points[i] = {
1194:                 'latitude'  : Dmarkers[i].getLatLng().lat,
1195:                 'longitude' : Dmarkers[i].getLatLng().lng
1196:             };
1197:             if (i == 0) {
1198:                 lat0 = lat1 = points[i].latitude;
1199:                 lng0 = lng1 = points[i].longitude;
1200:             }
1201:             if (lat0 > points[i].latitude)  lat0 = points[i].latitude;
1202:             if (lng0 > points[i].longitude) lng0 = points[i].longitude;
1203:             if (lat1 < points[i].latitude)  lat1 = points[i].latitude;
1204:             if (lng1 < points[i].longitude) lng1 = points[i].longitude;
1205:         }
1206:         map.fitBounds([[lat0, lng0], [lat1, lng1]]);
1207:     }

ユーザー関数 fitting は、「フィット」ボタンをクリックした時に呼び出される。すべてのマーカーが地図上に収まるように、ズームと中心点を最適化する。
変数 Dmarkers をスキャンして、マーカー群の南西端と北東端を求め、これをGooleマップAPIの fitBounds メソッドに渡すことで実現している。

解説:住所検索でマーカー追加

1416: //緯度・経度を取得する.
1417: else if (($errmsg == ''&& isButton('exec')) {
1418:     if ($query !'') {
1419:         list($n, $url) = $pgc->searchPoint3($query, GEOSERVICE, $category);
1420:         if ($pgc->iserror()) {
1421:             $errmsg = $pgc->geterror();
1422:         } else {
1423:             //終点マーカーを追加
1424:             list($latitude1, $longitude1, $address1) = $pgc->getPoint(1);
1425:             $arr = json_decode($points);
1426:             $n = ($arr == NULL? 0 : count($arr);
1427:             $arr[$n]['latitude']  = $latitude1;
1428:             $arr[$n]['longitude'] = $longitude1;
1429:             $points = json_encode($arr);
1430:             //地図のセンタリング
1431:             if (CENTERING) {
1432:                 $latitude0  = $latitude1;
1433:                 $longitude0 = $longitude1;
1434:             }
1435:         }
1436:     }
1437: }

検索キー(住所やランドマーク)を使ってマーカーを追加する処理はPHP側で行う。ジオコーディングWebAPIを呼び出す方法は「PHPで住所・ランドマークから緯度・経度を求める」で紹介したとおりで、取得した緯度・経度は、JSONテキストに保存してあるマーカー位置情報の最後に追加する。

活用例

地図上で目的地までの距離を測る:みんなの知識 ちょっと便利帳」では、このサンプル・プログラムを活用し、より使いやすいUIを実装している。ありがとうございます。

参考サイト

(この項おわり)
header