PHPで地図上の距離計測

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

(2026年1月11日) PHP8.5対応:double→float
(2025年8月30日).pahooEnv導入
(2025年6月14日)GoogleMaps JavaScript 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.5.0 2026/01/11 PHP8.5対応:double→float
2.4.0 2025/08/30 .pahooEnv導入
2.3.0 2023/08/12 検索キーの最小・最大長の指定
2.2.0 2023/08/12 国土地理院ジオコーディングAPIを追加
2.1 2021/12/04 Leaflet対応
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
6.9.1 2025/11/25 PHP8.5対応:double→float
6.9.0 2025/09/21 jsPolygon, jsPolygon_Gmap, jsPolygon_Leaflet, loadGeoJSON, getPrefBorderList を追加
6.8.0 2025/08/10 アクセスキーなどを ".pahooEnd" に分離
6.7.1 2025/07/26 jsLine_Gmap() - bug-fix
6.7.0 2025/07/20 drawJSmap,drawGMap -- 引数 $markerLevel 追加
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加
1.8.1 2025/03/15 validRegexPattern() debug
1.8.0 2024/11/12 validRegexPattern() 追加

準備:PHP の https対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

準備:pahooGeoCode クラス

pahooGeoCode.php

  41: class pahooGeoCode {
  42:     public $items;      // 検索結果格納用
  43:     public $error;      // エラー・フラグ
  44:     public $errmsg;     // エラー・メッセージ
  45:     public $hits;       // 検索ヒット件数
  46:     public $webapi; // 直前に呼び出したWebAPI URL
  47: 
  48:     // 都道府県境界線データ
  49:     // SimpleMaps.com is a product of Pareto Software, LLC. © 2010-2025
  50:     // https://simplemaps.com/gis/country/jp
  51:     // ※各自の環境に合わせて設定すること
  52:     public $GeoJsonJP = __DIR__ . '/jp.json';
  53: 
  54:     // -- 以下のデータは .env ファイルに記述可能
  55:     // Google Cloud Platform APIキー
  56:     // https://cloud.google.com/maps-platform/
  57:     // ※Google Maps APIを利用しないのなら登録不要
  58:     public $GOOGLE_API_KEY_1 = '';      // HTTPリファラ用
  59:     public $GOOGLE_API_KEY_2 = '';      // IP制限用
  60:     public $GOOGLE_MAP_ID    = '';      // GoogleMaps ID
  61: 
  62:     // Yahoo! JAPAN Webサービス アプリケーションID
  63:     // https://e.developer.yahoo.co.jp/register
  64:     // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  65:     public $YAHOO_APPLICATION_ID = '';
  66: 
  67:     // OSM Nominatim Search API利用時に知らせるメールアドレス
  68:     // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
  69:     // ※OSM Nominatim Search APIを利用しないのなら登録不要
  70:     public $NOMINATIM_EMAIL = '';
  71: 
  72:     // IP2Location.io APIキー
  73:     // https://www.ip2location.io/
  74:     // ※IP2Location.ioを利用しないのなら登録不要
  75:     public $IP2LOCATION_API_KEY = '';

GoogleマップやLeafletなどによる地図描画や住所検索を行うためのクラスが pahooGeoCode である。同梱のクラス・ファイル "pahooGeoCode.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooGeoCodeクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。

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

PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

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

weeklyCalendar.php

  65: // 地図描画サービスの選択
  66: //    0:Google
  67: //    2:地理院地図・OSM
  68: define('MAPSERVICE', 2);
  69: 
  70: // 住所検索サービスの選択
  71: //    0:Google
  72: //    1:Yahoo!JAPAN
  73: //   11:HeartRails Geo API
  74: //   12:OSM Nominatim Search API
  75: //   13:国土地理院ジオコーディングAPI
  76: define('GEOSERVICE', 1);
  77: 
  78: // 逆ジオコーディングサービスの選択
  79: //    0:Google
  80: //    1:Yahoo!JAPAN
  81: //   11:HeartRails Geo API
  82: //   21:簡易ジオコーディングサービス
  83: 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を初期化する。

解説:初期値

getDistance.php

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

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

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

解説:大圏航路の軌跡

getDistance.php

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

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

解説:大圏航路の距離

getDistance.php

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

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

解説:距離TABLE、経路TABLE

getDistance.php

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

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

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

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

getDistance.php

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

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

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

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

getDistance.php

 515:         }
 516:         writeDtable(Distances);         // 距離TABLE更新
 517:     }
 518: 
 519:     /**
 520:      * 軌跡の最後にマーカーを1つ追加する
 521:      * @param   LatLng point 追加する座標
 522:      * @return  なし
 523:     */
 524:     function addMarker(e) {

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

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

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

getDistance.php

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

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

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

getDistance.php

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

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

活用例

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

参考サイト

(この項おわり)
header