C++ で直近の地震情報を取得する

(1/1)
>C++で直近の地震情報を取得する
インターネット経由で気象庁防災情報XMLにアクセスし、最近の地震情報(発生日時、震源の位置・深さ、地震の規模)を地図上にマッピングしたり、その情報をファイル保存するアプリケーションを作る。「PHPで最近の地震情報を表示する」で作ったPHPプログラムをC++に移植したものである。

(2024年3月16日)使用ライブラリ更新
(2023年11月11日)使用ライブラリ更新
(2023年7月2日)使用ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
earthquakewin.msiインストーラ
bin/earthquakewin.exe実行プログラム本体
bin/cwebpage.dll
bin/libcurl.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/earthquakewin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ソース)
sour/pahooGeocode.hpp住所・緯度・経度に関わるクラス(ヘッダ)
sour/apikey.cppAPIキーの管理(ソース)
sour/apikey.hppAPIキーの管理(ヘッダ)
sour/webbrowser.hppWebブラウザ・クラス(ヘッダ)
sour/pahooCache.cppキャッシュ処理に関わるクラス(ソース)
sour/pahooCache.hppキャッシュ処理に関わるクラス(ヘッダ)
sour/makefileビルド
earthquakewin.cpp 更新履歴
バージョン 更新日 内容
3.4.5 2024/03/16 使用ライブラリ更新
3.4.4 2023/11/11 使用ライブラリ更新
3.4.3 2023/07/02 使用ライブラリ更新
3.4.2 2023/03/18 使用ライブラリ更新
3.4.1 2022/11/12 使用ライブラリ更新
pahooGeocode.cpp 更新履歴
バージョン 更新日 内容
1.6.0 2023/07/02 getPointsGSI()メソッド追加
1.5 2022/09/03 デバッグコード埋め込み
1.4 2021/05/01 makeMapLeaflet() 引数追加
1.3 2021/03/07 tokyo_wgs84(), wgs84_tokyo()メソッド追加
1.2 2020/12/20 leafletスクリプトの参照URL変更
pahooCache.cpp 更新履歴
バージョン 更新日 内容
1.0 2021/04/14
mystrings.cpp 更新履歴
バージョン 更新日 内容
1.12 2021/01/31 readWebContents() 引数post追加
1.11 2020/10/17 htmlspecialchars() 追加
1.1 2020/10/17 GetVersion2()追加,readWebContents() 引数ua追加
1.01 2020/10/03 setClipboardData() bug-fix
1.0 2020/09/22

使用ライブラリ

気象庁防災情報XMLにアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。
また、地図表示にWebブラウザ・コントロールを利用するため "cwebpage.dll" を利用する。codeproject からダウンロードできる。

リソースの準備

cwebpage.dll が32ビット対応であるため、今回は32ビット版の開発環境を用いる。
Eclipse を起動したら、新規プロジェクト earthquakewin を用意する。
ResEdit を起動したら、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "earthquakewin.cpp" を追加する。
リンカー・フラグを -Wl,--enable-stdcall-fixup -mwindows -lgdiplus -lole32 -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl "C:\(libcurl.dllのフォルダ)\libcurl.dll" "C:\(cwebpage.dllのフォルダ)\cwebpage.dll" に設定する。
また、コマンド行パターンをアレンジし "${COMMAND} ${FLAGS} ${OUTPUT_FLAG} ${OUTPUT_PREFIX}${OUTPUT} ${INPUTS} -luuid -loleaut32 -lole32" とする。
MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

解説:定数など

  40: #define MAKER       "pahoo.org"             //作成者
  41: #define APPNAME     "earthquakewin"         //アプリケーション名
  42: #define APPNAMEJP   "最近の地震情報"        //アプリケーション名(日本語)
  43: #define APPVERSION  "3.4.5"                 //バージョン
  44: #define APPYEAR     "2020-24"               //作成年
  45: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-12-01.shtm"   // 参考サイト
  46: 
  47: //char*バッファサイズ
  48: #define SIZE_BUFF   512
  49: 
  50: //現在のインターフェイス
  51: HINSTANCE hInst;
  52: 
  53: //アプリケーション・ウィンドウ
  54: HWND hParent;
  55: 
  56: //アプリケーション・ウィンドウ位置
  57: unsigned hParent_X, hParent_Y;
  58: 
  59: //エラー・メッセージ格納用【変更不可】
  60: string ErrorMessage;
  61: 
  62: //ListViewItemの最大文字長【変更不可】
  63: #define MAX_LISTVIEWITEM    259
  64: 
  65: //ヘルプ・ファイル
  66: #define HELPFILE    ".\\etc\\help.chm"
  67: 
  68: //デフォルト保存ファイル名
  69: #define SAVEFILE    "eq_%04d%02d%02d_%02d%02d.csv"

  96: //マップID
  97: #define MAP_ID          "map_id"
  98: //地図の大きさ
  99: #define MAP_WIDTH       600     //地図の幅(ピクセル)
 100: #define MAP_HEIGHT      400     //地図の高さ(ピクセル)
 101: //経度・緯度(初期値)
 102: #define DEF_LONGITUDE   139.766667
 103: double Longitude = DEF_LONGITUDE;
 104: #define DEF_LATITUDE    35.681111
 105: double Latitude  = DEF_LATITUDE;
 106: //地図拡大率(初期値)
 107: #define DEF_ZOOM        6
 108: int Zoom = DEF_ZOOM;
 109: //地図の種類(初期値)
 110: #define DEF_MAPTYPE     "GSISTD"
 111: string Maptype = DEF_MAPTYPE;
 112: #define INFO_WIDTH  (int)(MAP_WIDTH * 0.75//情報ウィンドウの最大幅
 113: #define INFO_OFFSET_X   0               //情報ウィンドウのオフセット位置(X)
 114: #define INFO_OFFSET_Y   -10             //情報ウィンドウのオフセット位置(Y)
 115: 
 116: //地震情報の最大格納数
 117: #define MAX_EARTHQUAKE  100

とくに注意記載が無い限り、定数は自由に変更できる。

解説:データ構造

 122: //地震情報を格納する構造体
 123: struct _Earthquake {
 124:     int id              = 0;            //マッピングID
 125:     int year            = 0;            //西暦年
 126:     int month           = 0;            //月
 127:     int day             = 0;            //日
 128:     int hour            = 0;            //時
 129:     int minuite         = 0;            //分
 130:     wstring location    = L"";          //震源地
 131:     double latitude     = 0.0;          //緯度
 132:     double longitude    = 0.0;          //経度
 133:     double depth        = 0.0;          //深さ
 134:     double magnitude    = -1.0;         //マグニチュード
 135:     int maxintensity    = -1;           //最大震度
 136: Earthquake[MAX_EARTHQUAKE];

取得した地震情報は構造体 Earthquake に格納する。

解説:キャッシュ・システム

最近の地震情報を取得するには、気象庁防災情報XMLから
  1. Atomフィード:高頻度フィード:地震火山
  2. Atomフィード:長期フィード:地震火山
  3. VXSE53
の3つのXMLファイルを読み込む必要がある。毎回、ロードすることは気象庁サイトへ負荷を掛けることになる。そこで、PHPプログラムの場合と同様、一定時間、自サイトにXMLファイルを保持しておきキャッシュ・システムを導入した。

  71: //キャッシュ・ディレクトリ
  72: #define DIR_CACHE_FEED      "pcache1\\"
  73: #define DIR_CACHE_FEED_L    "pcache2\\"
  74: #define DIR_CACHE_DATA      "pcache3\\"
  75: 
  76: //キャッシュ保持時間(デフォルト;分)(0:キャッシュしない)
  77: #define LIFE_CACHE_FEED     5       //高頻度フィードに対して
  78: #define LIFE_CACHE_FEED_L   120     //長期フィードに対して
  79: #define LIFE_CACHE_DATA     720     //地震情報に対して

キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。天気予報系プログラムと別のキャッシュ・ディレクトリにした方が、お互いのキャッシュ保持時間の干渉を受けなくなる。
配布ファイルは、新しい地震情報が入ってくることを考え、フィードの方を短く、地震情報の方は長くキャッシュ保持時間を設定してある。

解説:ワイド文字列中の改行を他の文字列に置換

 315: /**
 316:  * ワイド文字列中の改行を他の文字列に置換する
 317:  * @param  wstring str 置換対象の文字列
 318:  * @param  wstring rep 置換文字列
 319:  * @return wstring 置換後の文字列
 320:  */
 321: wstring wrepNL(wstring str, wstring rep) {
 322:     wstring strRet;
 323:     wstring::iterator ite = str.begin();
 324:     wstring::iterator iteEnd = str.end();
 325: 
 326:     if (0 < str.size()) {
 327:         wchar_t bNextChar = *ite++;
 328:         while (1) {
 329:             if (L'\r' == bNextChar) {
 330:                 // 改行確定
 331:                 strRet +rep;
 332:                 // EOF判定
 333:                 if (ite == iteEnd) {
 334:                     break;
 335:                 }
 336:                 // 1文字取得
 337:                 bNextChar = *ite++;
 338:                 if (L'\n' == bNextChar) {
 339:                     // EOF判定
 340:                     if (ite == iteEnd) {
 341:                         break;
 342:                     }
 343:                     // 1文字取得
 344:                     bNextChar = *ite++;
 345:                 }
 346:             } else if (L'\n' == bNextChar) {
 347:                 // 改行確定
 348:                 strRet +rep;
 349:                 // EOF判定
 350:                 if (ite == iteEnd) {
 351:                     break;
 352:                 }
 353:                 // 1文字取得
 354:                 bNextChar = *ite++;
 355:                 if (L'\r' == bNextChar) {
 356:                     // EOF判定
 357:                     if (ite == iteEnd) {
 358:                         break;
 359:                     }
 360:                     // 1文字取得
 361:                     bNextChar = *ite++;
 362:                 }
 363:             } else {
 364:                 // 改行以外
 365:                 strRet +bNextChar;
 366:                 // EOF判定
 367:                 if (ite == iteEnd) {
 368:                     break;
 369:                 }
 370:                 // 1文字取得
 371:                 bNextChar = *ite++;
 372:             }
 373:         };
 374:     }
 375:     return strRet;
 376: }

よく使う文字列処理関数は "mystrings.cpp" に分離した。
PHPの組み込み関数  nl2br  に相当する機能をワイド文字列用に拡張したのが wrepNL である。iterator を使ってワイド文字列を総なめにしている。

解説:地震情報取得

 489: /**
 490:  * 地震情報取得(気象庁防災情報XMLから)
 491:  * @param   なし
 492:  * @return  bool TRUE:取得成功/FALSE:失敗
 493: */
 494: bool getEarthquake(void) {
 495:     //年月日時分
 496:     regex re1("([0-9]+)\\-([0-9]+)\\-([0-9]+)T([0-9]+)\\:([0-9]+)");
 497:     //緯度・経度・深さ
 498:     regex re2("([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\-\\+\\/])([0-9]*)");
 499:     smatch mt1, mt2;
 500:     double lng, lat;
 501:     pahooCache* pCC;    //pahooCacheオブジェクト
 502: 
 503:     //地震情報URLを取得
 504:     static string urls[MAX_EARTHQUAKE];
 505:     bool res = jma_getLastEarthquakeURLs(urls);
 506:     if (res == FALSE) {
 507:         return FALSE;
 508:     }
 509: 
 510:     static string contents = "";
 511:     pCC = new pahooCache(LIFE_CACHE_DATA, getMyPath(APPNAME+ DIR_CACHE_DATA, UserAgent);
 512:     for (int i = 0i < MAX_EARTHQUAKEi++) {
 513:         if (urls[i] == "") {
 514: //          cout << "count = " << i << endl;
 515:             break;
 516:         }
 517: //      readWebContents(urls[i], UserAgent, &contents);
 518:         if (pCC->load(urls[i], &contents) == FALSE) {
 519:             ErrorMessage = _WS(pCC->getError());
 520:             return FALSE;
 521:         }
 522:         //XML読み込み
 523:         try {
 524:             std::stringstream ss;
 525:             ss << contents;
 526:             ptree pt;
 527:             xml_parser::read_xml(ss, pt);
 528: 
 529:             //XML解釈
 530:             //地震発生日時の取得
 531:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.OriginTime")) {
 532: //              cout << str.value() << " ";
 533:                 if (regex_search(str.value(), mt1, re1)) {
 534:                     Earthquake[i].year    = stoi(mt1[1].str());
 535:                     Earthquake[i].month   = stoi(mt1[2].str());
 536:                     Earthquake[i].day     = stoi(mt1[3].str());
 537:                     Earthquake[i].hour    = stoi(mt1[4].str());
 538:                     Earthquake[i].minuite = stoi(mt1[5].str());
 539:                 }
 540:             }
 541:             //震源地の取得
 542:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.Name")) {
 543:                 Earthquake[i].location = _UW(str.value());
 544: //              cout << _WS(Earthquake[i].location) << "  ";
 545:             }
 546:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.jmx_eb:Coordinate")) {
 547:                 if (regex_search(str.value(), mt2, re2)) {
 548:                     pGC->tokyo_wgs84(stod(mt2[2].str()), stod(mt2[1].str()), &lng, &lat);
 549:                     Earthquake[i].latitude  = lat;
 550:                     Earthquake[i].longitude = lng;
 551:                     if (mt2[3].str() == "/") {
 552:                         Earthquake[i].depth = (-1.0);
 553:                     } else {
 554:                         Earthquake[i].depth = stod(mt2[4].str());
 555:                     }
 556: //                  cout << "lat=" << lat << " lng=" << lng << " ";
 557:                 }
 558:             }
 559:             //マグニチュードの取得
 560:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.jmx_eb:Magnitude")) {
 561:                 Earthquake[i].magnitude = stod(str.value());
 562: //              cout << "M=" << Earthquake[i].magnitude << " ";
 563:             }
 564:             //最大震度の取得
 565:             if (optional<string>str = pt.get_optional<string>("Report.Body.Intensity.Observation.MaxInt")) {
 566:                 Earthquake[i].maxintensity = stoi(str.value());
 567: //              cout << "max=" << Earthquake[i].maxintensity << endl;
 568:             }
 569:         //読み込みエラー
 570:         } catch(xml_parser_error& e) {
 571:             ErrorMessage = "気象庁防災情報XMLにアクセスできません";
 572:             return FALSE;
 573:         }
 574:         contents.clear();
 575:     }
 576:     delete pCC;
 577: 
 578:     return TRUE;
 579: }

気象庁防災情報XMLからコンテンツを取り込むには、readWebContents で読み込んだXMLファイルを解釈していく。
XMLファイルの構造については、「PHPで直近の地震情報を表示する」をご覧いただきたい。

今回も、ワイド文字列に対する正規表現を使うことにした。ソースはSJISで書いているので、ユーザーマクロ関数 _SW を使ってワイド文字列に変換し、これを使って正規表現によるパターンマッチングを行う。

解説:マップを生成する

 809:             this->Ppoints[cnt].longitude = -d2;
 810:         } else if ((s2 == "N"|| (s2 == "n")) {
 811:             this->Ppoints[cnt].latitude = d2;
 812:         } else if ((s2 == "S"|| (s2 == "s")) {
 813:             this->Ppoints[cnt].latitude = -d2;
 814:         }
 815:         cnt++;
 816:     } else if (regex_search(query, mt1, re12)) {
 817:         cnt = 0;
 818:         string s1 = _WS(mt1[1].str());
 819:         string s2 = _WS(mt1[3].str());
 820:         double d1 = stod(mt1[2].str());
 821:         double d2 = stod(mt1[4].str());
 822:         if ((s1 == "E"|| (s1 == "e")) {
 823:             this->Ppoints[cnt].longitude = d1;
 824:         } else if ((s1 == "S"|| (s1 == "s")) {
 825:             this->Ppoints[cnt].longitude = -d1;
 826:         } else if ((s1 == "N"|| (s1 == "n")) {
 827:             this->Ppoints[cnt].latitude = d1;
 828:         } else if ((s1 == "S"|| (s1 == "s")) {
 829:             this->Ppoints[cnt].latitude = -d1;
 830:         }
 831:         if ((s2 == "E"|| (s2 == "e")) {
 832:             this->Ppoints[cnt].longitude = d2;
 833:         } else if ((s2 == "S"|| (s2 == "s")) {
 834:             this->Ppoints[cnt].longitude = -d2;
 835:         } else if ((s2 == "N"|| (s2 == "n")) {
 836:             this->Ppoints[cnt].latitude = d2;
 837:         } else if ((s2 == "S"|| (s2 == "s")) {
 838:             this->Ppoints[cnt].latitude = -d2;
 839:         }
 840:         cnt++;
 841:     } else if (regex_search(query, mt1, re13)) {
 842:         cnt = 0;
 843:         string s1 = _WS(mt1[1].str());
 844:         string s2 = _WS(mt1[3].str());
 845:         double d1 = stod(mt1[2].str());
 846:         double d2 = stod(mt1[4].str());
 847:         if ((s1 == ""|| (s1 == "+")) {
 848:             this->Ppoints[cnt].latitude = d1;
 849:         } else if (s1 == "-") {
 850:             this->Ppoints[cnt].latitude = -d1;
 851:         }
 852:         if ((s2 == ""|| (s2 == "+")) {
 853:             this->Ppoints[cnt].longitude = d2;
 854:         } else if (s2 == "-") {
 855:             this->Ppoints[cnt].longitude = -d2;
 856:         }
 857:         cnt++;
 858: 
 859:     //APIを自動選定
 860:     } else if (api == 0) {
 861:         for (int api : apilist) {
 862:             this->resetError();
 863:             cnt = this->__searchPoints(query, ua, api);
 864:             if (cnt > 0)    break;
 865:         }
 866:     //API指定
 867:     } else {
 868:         cnt = this->__searchPoints(query, ua, api);
 869:     }
 870: 
 871:     return cnt;
 872: }
 873: 
 874: // 地図描画 =================================================================
 875: /**
 876:  * 地図描画スクリプトを生成する
 877:  * @param   string id        マップID
 878:  * @param   double longitude 中心座標:経度(世界測地系)
 879:  * @param   double latitude  中心座標:緯度(世界測地系)
 880:  * @param   int    zoom      拡大率
 881:  * @param   string type      マップタイプ
 882:  *                              GSISTD:地理院地図(標準):省略時
 883:  *                              GSIPALE:地理院地図(淡色地図)
 884:  *                              GSIBLANK:地理院地図(白地図)
 885:  *                              GSIPHOTO:地理院地図(写真)
 886:  *                              OSM:OpenStreetMap
 887:  *                              GMRD:Googleマップ(ROADMAP);APIキー有効時
 888:  * @param   ppoints_t* items  地点情報配列(省略可能)
 889:  * @param   size_t size       地点情報配列の数(省略可能)
 890:  * @param   string call1      イベント発生時にコールする関数(省略可)
 891:  * @param   string call2      追加スクリプト(省略可)
 892:  * @param   int    max_width  情報ウィンドウの最大幅(省略時:200)
 893:  * @param   int    ofst_x     情報ウィンドウのオフセット位置(X)(省略時:0)
 894:  * @param   int    ofst_y     情報ウィンドウのオフセット位置(Y)(省略時:0)
 895:  * @return  string 生成したスクリプト
 896:  */
 897: string pahooGeocode::makeMapLeaflet(
 898:     string id, double longitude, double latitude, int zoom,
 899:     string type, ppoints_t* items, size_t size, string call1, string call2,
 900:     int max_width, int ofst_x, int ofst_y) {
 901: 
 902:     string ss1 = __GMAP_TILE1;
 903:     string ss2 = __GMAP_TILE2;
 904: 
 905:     //地点情報スクリプトの生成
 906:     char lat[SIZE_BUFF + 1], lng[SIZE_BUFF + 1];
 907:     char buff[SIZE_BUFF + 1];
 908:     string icode = "";
 909:     if (items !NULL) {
 910:         string icon = "";
 911:         string info = "";
 912:         for (size_t i = 0i < sizei++) {
 913:             if ((items[i].icon) == "" && (i > 25))  break;
 914:                                     //アイコンURLなく 'Z'を超えたら打ち止め
 915:             string mark = {(char)(65 + i)};
 916:             if (items[i].icon == "") {
 917:                 icon = "https://www.google.com/mapfiles/marker" + mark + ".png";
 918:             } else {
 919:                 icon = items[i].icon;
 920:             }
 921:             info = "";
 922:             if (items[i].description !L"") {
 923:                 snprintf(buff, SIZE_BUFF, "', {maxWidth: %d, offset: [%d, %d] });", max_width, ofst_x, ofst_y);
 924:                 info = "marker_" + mark + ".bindPopup('" + _WS(items[i].description+ buff;
 925:             }
 926:             snprintf(lat, SIZE_BUFF, "%.5f", items[i].latitude);
 927:             snprintf(lng, SIZE_BUFF, "%.5f", items[i].longitude);
 928:             icode += (boost::format(R"(
 929:                 var icon_%1% =  new L.icon({
 930:                     iconUrl: '%2%',
 931:                     iconAnchor: [10, 10]    //暫定
 932:                 });
 933:                 var marker_%1% = new L.Marker([%3%, %4%], {icon: icon_%1%}).addTo(map);
 934:                 %5%
 935: )")
 936:  %mark           //マーカー識別子
 937: % icon          //マーカーURL
 938: % lat           //緯度
 939: % lng           //経度
 940: % info          //情報
 941: ).str();
 942:         }
 943:     }
 944: 
 945:     //地図描画スクリプトの生成
 946:     string script = (boost::format(R"(
 947: %6%
 948: <link rel="stylesheet" href="https://unpkg.com/leaflet@latest/dist/leaflet.css" />
 949: <script src="https://unpkg.com/leaflet@latest/dist/leaflet.js"></script>
 950: %7%
 951: <script>
 952: window.onload = function() {
 953:     var map = L.map('%1%',{zoomControl:false});
 954:     map.setView([%2%, %3%], %4%);
 955:     L.control.scale({
 956:         maxWidth: 250,
 957:         position: 'bottomright',
 958:         imperial: false
 959:     }).addTo(map);
 960:     L.control.zoom({position:'topleft'}).addTo(map);
 961: 
 962:     //地理院地図:標準地図
 963:     var GSISTD = new L.tileLayer(
 964:         'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
 965:         {
 966:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 967:             minZoom: 0,
 968:             maxZoom: 18,
 969:             name: 'GSISTD'
 970:         });
 971:     //地理院地図:淡色地図
 972:     var GSIPALE = new L.tileLayer(
 973:         'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
 974:         {
 975:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 976:             minZoom: 2,
 977:             maxZoom: 18,
 978:             name: 'GSIPALE'
 979:         });
 980:     //地理院地図:白地図
 981:     var GSIBLANK = new L.tileLayer(
 982:         'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
 983:         {
 984:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 985:             minZoom: 5,
 986:             maxZoom: 14,
 987:             name: 'GSIBLANK'
 988:         });
 989:     //地理院地図:写真
 990:     var GSIPHOTO = new L.tileLayer(
 991:         'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
 992:         {
 993:             attribution: "<a href='https://maps.gsi.go.jp/development/ichiran.html' target='_blank'>地理院タイル</a>",
 994:             minZoom: 2,
 995:             maxZoom: 18,
 996:             name: 'GSIPHOTO'
 997:         });
 998:     //OpenStreetMap
 999:     var OSM = new L.tileLayer(
1000:         'https://tile.openstreetmap.jp/{z}/{x}/{y}.png',
1001:         {
1002:             attribution: "<a href='https://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors",
1003:             minZoom: 0,
1004:             maxZoom: 18,
1005:             name: 'OSM'
1006:         });
1007: %12%
1008: %8%
1009: 
1010:     //baseMapsオブジェクトにタイル設定

地図描画は "pahooGeocode.cpp" に分離し、クラス pahooGeocode のメソッド makeMapLeaflet としている。
このメソッドは、「地理院地図・OSM描画 -PHPで住所・ランドマークから最寄り駅を求める」で紹介した手法をそのまま移植した。無償のJavaScriptライブラリLeafletを利用している。
後述するように、Googleマップが利用できるときには、必要なスクリプトを変数 GoogleMap1GoogleMap4 から追加するようにした。

解説:Googleマップを利用する

Google Cloud Platform - 各種WebAPIの登録方法」で紹介したように、Googleマップを利用するには、APIキーを取得する必要がある。Google Cloud Platform は利用量によって課金される。現在、Googleマップ関連サービスは毎月200ドルまでは無料だが、それ以上の利用量があると課金対象となり、登録したクレジットカードに請求される。
そこで本プログラムでは、ユーザーが APIキーを取得した場合、それをプログラムから入力・保存できるようにするダイアログを用意した。

 102: /**
 103:  * AppDataのパスを取得
 104:  * @param   char* appname アプリケーション名
 105:  * @return  TCHAR* パス
 106:  */
 107: TCHAR* pahooGeocode::getMyPath(const char* appname) {
 108:     static TCHAR myPath[MAX_PATH] = "";
 109: 
 110:     if (strlen(myPath) == 0) {
 111:         if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
 112:             TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
 113:             if (ptmp !NULL) {
 114:                 ptmp = _tcsinc(ptmp);
 115:                 *ptmp = _T('\0');
 116:             }
 117:             strcat(myPath, _T("Roaming"));
 118:             CreateDirectory((LPCTSTR)myPath, NULL);
 119:             strcat(myPath, _T("\\pahoo.org"));
 120:             CreateDirectory((LPCTSTR)myPath, NULL);
 121:             strcat(myPath, _T("\\"));
 122:             strcat(myPath, _T(appname));
 123:             CreateDirectory((LPCTSTR)myPath, NULL);
 124:             strcat(myPath, _T("\\"));
 125:         } else {
 126:         }
 127:     }
 128:     return myPath;
 129: }

APIキーの保存場所は、AppDataの下に、"pahoo.org\(アプリケーション名)" というフォルダを用意し、ここにテキストファイルとして書き込む。管理者権限がないと "Program Files" に書き込めないので、AppData を利用するというWindowsのお作法に則った。
なお、WiX を使って作ったアンインストーラーを実行すると、このフォルダを消去するようにしてある。

 188: /**
 189:  * Google Cloud Platform APIキーを書き込む
 190:  * @param   string key  書き込むAPIキー
 191:  * @return  bool TRUE:書込成功/FALSE:失敗
 192:  */
 193: bool pahooGeocode::writeGoogleApiKey(std::string key) {
 194:     bool ret = TRUE;
 195:     ofstream ofs;
 196: 
 197:     ofs.open((string)this->getMyPath(NULL+ FNAME_GOOGLE_API);
 198:     ofs << key;
 199:     if(ofs.bad()) {
 200:         this->errmsg = _SW("Google Cloud Platform APIキーの保存に失敗しました");
 201:         ret = FALSE;
 202:     }
 203:     ofs.close();
 204:     this->readGoogleApiKey();
 205: 
 206:     return ret;
 207: }

APIキーの保存を行うメソッドは writeGoogleApiKey である。

 132: /**
 133:  * Google Cloud Platform APIキーを読み込む
 134:  * @param   なし
 135:  * @return  bool TRUE:読込成功/FALSE:ファイルがない
 136:  */
 137: bool pahooGeocode::readGoogleApiKey(void) {
 138:     string key;
 139:     bool ret = FALSE;
 140: 
 141:     ifstream ifs((string)this->getMyPath(NULL+ this->FNAME_GOOGLE_API);
 142:     //APIキー・ファイルが無ければ初期化
 143:     if (!ifs) {
 144:         this->GoogleAPIkey = "";
 145:         this->GoogleMap1 = "";
 146:         this->GoogleMap2 = "";
 147:         this->GoogleMap3 = "";
 148:         this->GoogleMap4 = "";
 149:         return ret;
 150:     }
 151: 
 152:     //APIキー読み込み
 153:     ifs >> key;
 154:     if(ifs.bad()) {
 155:         this->errmsg = _SW("Google Cloud Platform APIキーの読み込みに失敗しました");
 156:         ifs.close();
 157:         this->GoogleAPIkey = "";
 158:         this->GoogleMap1 = "";
 159:         this->GoogleMap2 = "";
 160:         this->GoogleMap3 = "";
 161:         this->GoogleMap4 = "";
 162:         return FALSE;
 163:     }
 164:     ifs.close();
 165: 
 166:     //APIキーなどの設定
 167:     if (key.length() > 0) {
 168:         this->GoogleAPIkey = key;
 169:         this->GoogleMap1 = "<script src='https://maps.googleapis.com/maps/api/js?key="
 170:                 + key + "' async defer></script>";
 171:         this->GoogleMap2 =
 172:                 "<script src='https://unpkg.com/leaflet.gridlayer.googlemutant@0.10.2/Leaflet.GoogleMutant.js'></script>";
 173:         this->GoogleMap3 =
 174:                 "var GMRD = L.gridLayer.googleMutant({type:'roadmap', name:'GMRD'});\nvar GMST = L.gridLayer.googleMutant({type:'satellite', name:'GMST'});\nvar GMHB = L.gridLayer.googleMutant({type:'hybrid', name:'GMHB'});";
 175:         this->GoogleMap4 = ",'Googleマップ(標準)' : GMRD,\n'Googleマップ(写真)' : GMST,\n'Googleマップ(混合)' : GMHB";
 176:         ret = TRUE;
 177:         //APIキーが無ければ初期化
 178:     } else {
 179:         this->GoogleAPIkey = "";
 180:         this->GoogleMap1 = "";
 181:         this->GoogleMap2 = "";
 182:         this->GoogleMap3 = "";
 183:         this->GoogleMap4 = "";
 184:     }
 185:     return ret;
 186: }

APIキーの読み込みを行うメソッドは readGoogleApiKey である。
有効なキーがあれば、変数 GoogleAPIkey に代入する。また、前述の makeMapLeaflet メソッドにGoogleマップを追加するためのスクリプトを変数 GoogleMap1GoogleMap4 に代入する。
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

解説:マップ表示用HTML生成

 717: /**
 718:  * 地図表示用HTML生成
 719:  * @param   string 表示するインフォメーション
 720:  * @return  string 生成したHTML文
 721:  */
 722: string makeMapHTML(wstring info) {
 723: //  if (pGC->GoogleAPIkey.length() == 0) {
 724: //      Maptype = DEF_MAPTYPE;
 725: //  }
 726: 
 727:     int cnt = info2points();
 728:     string script = pGC->makeMapLeaflet(MAP_ID, Earthquake[0].longitude, Earthquake[0].latitude, Zoom, Maptype, pGC->Ppoints, cnt, "", "", INFO_WIDTH, INFO_OFFSET_X, INFO_OFFSET_Y);
 729: 
 730:     string html = (boost::format(R"(<!DOCTYPE html>
 731: <html lang="ja">
 732: <head>
 733: <meta charset="SJIS">
 734: <title>%9%</title>
 735: <meta name="author" content="studio pahoo" />
 736: <meta name="copyright" content="studio pahoo" />
 737: <meta name="ROBOTS" content="NOINDEX,NOFOLLOW" />
 738: <meta http-equiv="pragma" content="no-cache">
 739: <meta http-equiv="cache-control" content="no-cache">
 740: <meta http-equiv="X-UA-Compatible" content="IE=edge">
 741: %8%
 742: </head>
 743: <body>
 744: <div id="%1%" style="width:%2%px; height:%3%px;"></div>
 745: <form>
 746: <input id="latitude"  type="hidden" value="%4%" />
 747: <input id="longitude" type="hidden" value="%5%" />
 748: <input id="zoom"      type="hidden" value="%6%" />
 749: <input id="maptype"   type="hidden" value="%7%" />
 750: </form>
 751: </body>
 752: </html>
 753: )")
 754:  %MAP_ID                     //地図ID
 755: % MAP_WIDTH                     //地図の幅
 756: % MAP_HEIGHT                    //地図の高さ
 757: % Earthquake[0].latitude        //緯度
 758: % Earthquake[0].longitude       //経度
 759: % Zoom                          //地図拡大率
 760: % Maptype                       //地図タイプ
 761: % script                        //地図描画スクリプト
 762: % APPNAMEJP                     //アプリケーション名
 763: ).str();
 764: 
 765:     return html;
 766: }

マップ表示用HTML文を生成するのがユーザー関数 makeMapHTML である。
地震情報を引数にして、前述のメソッド makeMapLeaflet を呼び出してHTML文を生成する。

解説:ブラウザ・コントロール表示

 796: /**
 797:  * ブラウザ・コントロールを表示
 798:  * @param   wstring info 情報ウィンドウに表示するテキスト
 799:  * @param   char* tmpname 読み込むHTMLファイル名
 800:  * @return  なし
 801: */
 802: void viewBrowser(wstring info, const char* tmpname) {
 803:     string html;
 804:     string fname;
 805:     ofstream ofs;
 806: 
 807:     //表示用HTMLファイル作成
 808:     if ((ErrorMessage == ""&& (! pGC->isError())) {
 809:         html = makeMapHTML(info);
 810:     } else {
 811:         html = makeErrorHTML();
 812:     }
 813:     ofs.open(tmpname);
 814:     ofs << html;
 815:     ofs.close();
 816: 
 817:     fname = (string)tmpname;
 818:     std::replace(fname.begin(), fname.end(), '\\', '/');
 819:     wBrowser.loadPage((char*)fname.c_str());
 820:     wBrowser.fitToParent();
 821: }

ユーザー関数 makeMapHTML で生成されたHTML文をブラウザ・コントロールに表示するのがユーザー関数 viewBrowser である。
テンポラリディレクトリにHTML文を保存し、これを表示するようにしている。

1118:         //ブラウザ・コントロール作成
1119:         hHTML = CreateWindowEx(0, WC_STATIC, "WebBrowser", WS_CHILD | WS_VISIBLE | ES_LEFT, 10, 10, MAP_WIDTH + 20, MAP_HEIGHT + 20, hDlg, NULL, hInst, NULL);
1120:         wBrowser.create((char*)"", 0, 0, MAP_WIDTH + 20, MAP_HEIGHT + 20, hHTML);

ブラウザ・コントロールは、ダイアログを初期化するときに用意する。
WebBrowserコントロール・クラス WebBrowse は、Digital Point の "webbrowser.h" を参考にアレンジし、"webbrowser.hpp" とした。

解説:地図描画パラメータ

地図描画のパラメータとして、緯度は $Latitude、経度は $Longitude、拡大率は $Zoom、地図形式は $Maptype のグローバル変数に、それぞれ代入している。

 139: /**
 140:  * パラメータの初期化
 141:  * @param   なし
 142:  * @return  なし
 143:  */
 144: void initParameter(void) {
 145:     Longitude = DEF_LONGITUDE;
 146:     Latitude  = DEF_LATITUDE;
 147:     Zoom      = DEF_ZOOM;
 148:     Maptype   = DEF_MAPTYPE;
 149:     hParent_X = 0;
 150:     hParent_Y = 0;
 151: }

パラメータは、initParamete によって初期化する。

 216: /**
 217:  * パラメータの保存
 218:  * @param   なし
 219:  * @return  なし
 220:  */
 221: void saveParameter(void) {
 222: #ifndef CMDAPP
 223:     //アプリケーション・ウィンドウの位置取得
 224:     WINDOWINFO windowInfo;
 225:     windowInfo.cbSize = sizeof(WINDOWINFO);
 226:     GetWindowInfo(hParent, &windowInfo);
 227:     hParent_X = (unsigned)windowInfo.rcWindow.left;
 228:     hParent_Y = (unsigned)windowInfo.rcWindow.top;
 229:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 230:         hParent_X = 0;
 231:     }
 232:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 233:         hParent_Y = 0;
 234:     }
 235: #endif
 236: 
 237:     //ブラウザ・コントロールからパラメータを取り出す
 238:     wstring val;
 239:     if (wBrowser.getInputById(L"longitude", &val)) {
 240:         Longitude = stod(val);
 241:     }
 242:     if (wBrowser.getInputById(L"latitude", &val)) {
 243:         Latitude = stod(val);
 244:     }
 245:     if (wBrowser.getInputById(L"zoom", &val)) {
 246:         Zoom = stoi(val);
 247:     }
 248:     if (wBrowser.getInputById(L"maptype", &val)) {
 249:         Maptype = _WS(val);
 250:     }
 251: 
 252:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
 253:     snprintf(lng, SIZE_BUFF, "%.5f", Longitude);
 254:     snprintf(lat, SIZE_BUFF, "%.5f", Latitude);
 255: 
 256:     //XMLファイルへ書き込む
 257:     ptree pt;
 258:     ptree& child1 = pt.add("parameter.param", lat);
 259:     child1.add("<xmlattr>.type", "latitude");
 260:     ptree& child2 = pt.add("parameter.param", lng);
 261:     child2.add("<xmlattr>.type", "longitude");
 262:     ptree& child3 = pt.add("parameter.param", to_string(Zoom));
 263:     child3.add("<xmlattr>.type", "zoom");
 264:     ptree& child4 = pt.add("parameter.param", Maptype);
 265:     child4.add("<xmlattr>.type", "maptype");
 266:     ptree& child5 = pt.add("parameter.param", (string)to_string(hParent_X));
 267:     child5.add("<xmlattr>.type", "wx");
 268:     ptree& child6 = pt.add("parameter.param", (string)to_string(hParent_Y));
 269:     child6.add("<xmlattr>.type", "wy");
 270: 
 271:     const int indent = 4;
 272:     write_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt, std::locale(),
 273:         xml_writer_make_settings<std::string>(' ', indent));
 274: }

地図描画のパラメータは、地図に対する操作で随時変化する。この変化は、上述の地図描画JavaScriptによって、HTMLのINPUT要素の値として代入されている。
ブラウザ・コントロールから、これらの値を取り出し、所定のXMLファイルへ保存するのが saveParameter である。
保存場所は、"[C:\Users\(ユーザー名)\AppData\Roaming\pahoo.org\(アプリケーション名)" である。
なお、INPUT要素を取得する方法は、「INPUT要素の取得 - C++で最寄駅を検索」で紹介したとおりだ。

 153: /**
 154:  * パラメータの読み込み
 155:  * @param   なし
 156:  * @return  なし
 157:  */
 158: void loadParameter(void) {
 159:     ptree pt;
 160: 
 161:     //初期値設定
 162:     initParameter();
 163: 
 164:     //XMLファイル読み込み
 165:     try {
 166:         xml_parser::read_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt);
 167: 
 168:         //XML解釈
 169:         try {
 170:             //形式チェック
 171:             if (optional<string>str = pt.get_optional<string>("parameter")) {
 172:             } else {
 173:                 return;
 174:             }
 175:             //パラメータ読み込み
 176:             for (auto it : pt.get_child("parameter")) {
 177:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
 178:                 if (type == "latitude") {
 179:                     Latitude = stod(it.second.data());
 180:                 } else if (type == "longitude") {
 181:                     Longitude = stod(it.second.data());
 182:                 } else if (type == "zoom") {
 183:                     Zoom = stoi(it.second.data());
 184:                 } else if (type == "maptype") {
 185:                     Maptype = (string)it.second.data();
 186:                 } else if (type == "wx") {
 187:                     hParent_X = (unsigned)stoi(it.second.data());
 188:                 } else if (type == "wy") {
 189:                     hParent_Y = (unsigned)stoi(it.second.data());
 190:                 }
 191:             }
 192:         //解釈失敗したら初期値設定
 193:         } catch (xml_parser_error& e) {
 194:             initParameter();
 195:             return;
 196:         }
 197:     //読み込み失敗したら初期値設定
 198:     } catch (xml_parser_error& e) {
 199:         initParameter();
 200:         return;
 201:     }
 202: 
 203:     //アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
 204:     HWND hDesktop = GetDesktopWindow();
 205:     WINDOWINFO windowInfo;
 206:     windowInfo.cbSize = sizeof(WINDOWINFO);
 207:     GetWindowInfo(hDesktop, &windowInfo);
 208:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 209:         hParent_X = 0;
 210:     }
 211:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 212:         hParent_Y = 0;
 213:     }
 214: }

アプリケーション起動時に地図描画のパラメータを読み出す関数が loadParameter である。saveParameter によって保存されたXMLファイルがあれば、その値を読み込む。無ければ、initParamete によって初期化する。
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header