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

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

(2021 年 3 月 9 日)2021 年(令和 3 年)2 月 24 日の気象庁サイト・リニューアルにより、スクレイピングによる取り出しが難しくなったため、気象庁防災情報 XML からの取得に変更した。
(2021 年 1 月 2 日)pahooGeocode クラスを使用するなど、ソースを全面改訂。

目次

サンプル・プログラム

圧縮ファイルの内容
earthquakewin.msiインストーラ
bin/earthquakewin.exe実行プログラム本体
bin/cwebpage.dll
bin/libcrypto-1_1.dll
bin/libcurl.dll
bin/libssl-1_1.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キャッシュ処理に関わるクラス(ヘッダ)

使用ライブラリ

気象庁防災情報 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" とする。

解説:定数など

0040: #define MAKER     "pahoo.org"                //作成者
0041: #define APPNAME     "earthquakewin"            //アプリケーション名
0042: #define APPNAMEJP "最近の地震情報"        //アプリケーション名日本語)
0043: #define APPVERSION "3.1"                    //バージョン
0044: #define APPYEAR     "2020-21"                //作成年
0045: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-12-01.shtm"  // 参考サイト
0046: 
0047: //char*バッファサイズ
0048: #define SIZE_BUFF 512
0049: 
0050: //現在のインターフェイス
0051: HINSTANCE hInst;
0052: 
0053: //親ウィンドウ
0054: HWND hParent;
0055: 
0056: //エラー・メッセージ格納用【変更不可】
0057: string ErrorMessage;
0058: 
0059: //ListViewItemの最大文字長【変更不可】
0060: #define MAX_LISTVIEWITEM 259
0061: 
0062: //ヘルプ・ファイル
0063: #define HELPFILE ".\\etc\\help.chm"
0064: 
0065: //デフォルト保存ファイル名
0066: #define SAVEFILE "eq_%04d%02d%02d_%02d%02d.csv"

0093: //マップID
0094: #define MAP_ID         "map_id"
0095: //地図の大きさ
0096: #define MAP_WIDTH     600        //地図の幅(ピクセル)
0097: #define MAP_HEIGHT     400        //地図の高さ(ピクセル)
0098: //経度・緯度(初期値)
0099: #define DEF_LONGITUDE 139.766667
0100: double Longitude = DEF_LONGITUDE;
0101: #define DEF_LATITUDE 35.681111
0102: double Latitude  = DEF_LATITUDE;
0103: //地図拡大率(初期値)
0104: #define DEF_ZOOM     6
0105: int Zoom = DEF_ZOOM;
0106: //地図の種類(初期値)
0107: #define DEF_MAPTYPE     "GSISTD"
0108: string Maptype = DEF_MAPTYPE;
0109: #define INFO_WIDTH (int)(MAP_WIDTH * 0.75) //情報ウィンドウの最大幅
0110: #define INFO_OFFSET_X 0                //情報ウィンドウのオフセット位置(X)
0111: #define INFO_OFFSET_Y -10                //情報ウィンドウのオフセット位置(Y)
0112: 
0113: //地震情報の最大格納数
0114: #define MAX_EARTHQUAKE 100

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

解説:データ構造

0119: //地震情報を格納する構造体
0120: struct _Earthquake {
0121:     int id              = 0;         //マッピングID
0122:     int year            = 0;         //西暦年
0123:     int month           = 0;         //月
0124:     int day             = 0;         //日
0125:     int hour            = 0;         //時
0126:     int minuite         = 0;         //分
0127:     wstring location    = L"";          //震源地
0128:     double latitude     = 0.0;            //緯度
0129:     double longitude    = 0.0;            //経度
0130:     double depth        = 0.0;            //深さ
0131:     double magnitude    = -1.0;            //マグニチュード
0132:     int maxintensity    = -1;         //最大震度
0133: Earthquake[MAX_EARTHQUAKE];

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

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

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

0068: //キャッシュ・ディレクトリ
0069: #define DIR_CACHE_FEED     "pcache1\\"
0070: #define DIR_CACHE_FEED_L "pcache2\\"
0071: #define DIR_CACHE_DATA     "pcache3\\"
0072: 
0073: //キャッシュ保持時間(デフォルト;分)(0:キャッシュしない)
0074: #define LIFE_CACHE_FEED     5        //高頻度フィードに対して
0075: #define LIFE_CACHE_FEED_L 120        //長期フィードに対して
0076: #define LIFE_CACHE_DATA     720        //地震情報に対して

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

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

0315: /**
0316:  * ワイド文字列中の改行を他の文字列に置換する
0317:  * @param  wstring str 置換対象の文字列
0318:  * @param  wstring rep 置換文字列
0319:  * @return wstring 置換後の文字列
0320:  */
0321: wstring wrepNL(wstring strwstring rep) {
0322:     wstring strRet;
0323:     wstring::iterator ite = str.begin();
0324:     wstring::iterator iteEnd = str.end();
0325: 
0326:     if (0 < str.size()) {
0327:         wchar_t bNextChar = *ite++;
0328:         while (1) {
0329:             if (L'\r' == bNextChar) {
0330:                 // 改行確定
0331:                 strRet += rep;
0332:                 // EOF判定
0333:                 if (ite == iteEnd) {
0334:                     break;
0335:                 }
0336:                 // 1文字取得
0337:                 bNextChar = *ite++;
0338:                 if (L'\n' == bNextChar) {
0339:                     // EOF判定
0340:                     if (ite == iteEnd) {
0341:                         break;
0342:                     }
0343:                     // 1文字取得
0344:                     bNextChar = *ite++;
0345:                 }
0346:             } else if (L'\n' == bNextChar) {
0347:                 // 改行確定
0348:                 strRet += rep;
0349:                 // EOF判定
0350:                 if (ite == iteEnd) {
0351:                     break;
0352:                 }
0353:                 // 1文字取得
0354:                 bNextChar = *ite++;
0355:                 if (L'\r' == bNextChar) {
0356:                     // EOF判定
0357:                     if (ite == iteEnd) {
0358:                         break;
0359:                     }
0360:                     // 1文字取得
0361:                     bNextChar = *ite++;
0362:                 }
0363:             } else {
0364:                 // 改行以外
0365:                 strRet += bNextChar;
0366:                 // EOF判定
0367:                 if (ite == iteEnd) {
0368:                     break;
0369:                 }
0370:                 // 1文字取得
0371:                 bNextChar = *ite++;
0372:             }
0373:         };
0374:     }
0375:     return strRet;
0376: }

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

解説:地震情報取得

0437: /**
0438:  * 地震情報取得(気象庁防災情報XMLから)
0439:  * @param なし
0440:  * @return bool TRUE:取得成功/FALSE:失敗
0441: */
0442: int getEarthquake(void) {
0443:     //年月日時分
0444:     regex re1("([0-9]+)\\-([0-9]+)\\-([0-9]+)T([0-9]+)\\:([0-9]+)");
0445:     //緯度・経度・深さ
0446:     regex re2("([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)[\\-\\+]([0-9]+)");
0447:     smatch mt1mt2;
0448:     double lnglat;
0449:     pahooCachepCC; //pahooCacheオブジェクト
0450: 
0451:     //地震情報URLを取得
0452:     static string urls[MAX_EARTHQUAKE];
0453:     bool res = jma_getLastEarthquakeURLs(urls);
0454:     if (res == FALSE) {
0455:         return FALSE;
0456:     }
0457: 
0458:     static string contents = "";
0459:     pCC = new pahooCache(LIFE_CACHE_DATAgetMyPath(APPNAME) + DIR_CACHE_DATAUserAgent);
0460:     for (int i = 0; i < MAX_EARTHQUAKEi++) {
0461:         if (urls[i] == "") {
0462: //          cout << "count = " << i << endl;
0463:             break;
0464:         }
0465: //      readWebContents(urls[i], UserAgent, &contents);
0466:         if (pCC->load(urls[i], &contents) == FALSE) {
0467:             ErrorMessage = _WS(pCC->getError());
0468:             return FALSE;
0469:         }
0470:         //XML読み込み
0471:         try {
0472:             std::stringstream ss;
0473:             ss << contents;
0474:             ptree pt;
0475:             xml_parser::read_xml(sspt);
0476: 
0477:             //XML解釈
0478:             //地震発生日時の取得
0479:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.OriginTime")) {
0480: //              cout << str.value() << " ";
0481:                 if (regex_search(str.value(), mt1re1)) {
0482:                     Earthquake[i].year    = stoi(mt1[1].str());
0483:                     Earthquake[i].month   = stoi(mt1[2].str());
0484:                     Earthquake[i].day     = stoi(mt1[3].str());
0485:                     Earthquake[i].hour    = stoi(mt1[4].str());
0486:                     Earthquake[i].minuite = stoi(mt1[5].str());
0487:                 }
0488:             }
0489:             //震源地の取得
0490:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.Name")) {
0491:                 Earthquake[i].location = _UW(str.value());
0492: //              cout << _WS(Earthquake[i].location) << "  ";
0493:             }
0494:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.jmx_eb:Coordinate")) {
0495:                 if (regex_search(str.value(), mt2re2)) {
0496:                     pGC->tokyo_wgs84(stod(mt2[2].str()), stod(mt2[1].str()), &lng, &lat);
0497:                     Earthquake[i].latitude  = lat;
0498:                     Earthquake[i].longitude = lng;
0499:                     Earthquake[i].depth     = stod(mt2[3].str());
0500: //                  cout << "lat=" << lat << " lng=" << lng << " ";
0501:                 }
0502:             }
0503:             //マグニチュードの取得
0504:             if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.jmx_eb:Magnitude")) {
0505:                 Earthquake[i].magnitude = stod(str.value());
0506: //              cout << "M=" << Earthquake[i].magnitude << " ";
0507:             }
0508:             //最大震度の取得
0509:             if (optional<string>str = pt.get_optional<string>("Report.Body.Intensity.Observation.MaxInt")) {
0510:                 Earthquake[i].maxintensity = stoi(str.value());
0511: //              cout << "max=" << Earthquake[i].maxintensity << endl;
0512:             }
0513:         //読み込みエラー
0514:         } catch(xml_parser_errore) {
0515:             ErrorMessage = "気象庁防災情報XMLにアクセスできません";
0516:             return FALSE;
0517:         }
0518:         contents.clear();
0519:     }
0520:     delete pCC;
0521: 
0522:     return TRUE;
0523: }

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

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

解説:マップを生成する

0801: /**
0802:  * 地図描画スクリプトを生成する
0803:  * @param string id        マップID
0804:  * @param double longitude 中心座標:経度(世界測地系)
0805:  * @param double latitude  中心座標:緯度(世界測地系)
0806:  * @param int    zoom      拡大率
0807:  * @param string type      マップタイプ
0808:  *                              GSISTD:地理院地図(標準):省略時
0809:  *                              GSIPALE:地理院地図(淡色地図)
0810:  *                              GSIBLANK:地理院地図(白地図)
0811:  *                              GSIPHOTO:地理院地図(写真)
0812:  *                              OSM:OpenStreetMap
0813:  *                              GMRD:Googleマップ(ROADMAP);APIキー有効時
0814:  * @param ppoints_t* items  地点情報配列(省略可能)
0815:  * @param size_t size       地点情報配列の数(省略可能)
0816:  * @param string call1      イベント発生時にコールする関数(省略可)
0817:  * @param string call2      追加スクリプト(省略可)
0818:  * @param int    max_width  情報ウィンドウの最大幅(省略時:200)
0819:  * @param int    ofst_x     情報ウィンドウのオフセット位置(X)(省略時:0)
0820:  * @param int    ofst_y     情報ウィンドウのオフセット位置(Y)(省略時:0)
0821:  * @return string 生成したスクリプト
0822:  */
0823: string pahooGeocode::makeMapLeaflet(string iddouble longitudedouble latitudeint zoomstring typeppoints_titemssize_t sizestring call1string call2int max_widthint ofst_xint ofst_y) {
0824:     //地点情報スクリプトの生成
0825:     char lat[SIZE_BUFF + 1], lng[SIZE_BUFF + 1];
0826:     char buff[SIZE_BUFF + 1];
0827:     string icode = "";
0828:     if (items != NULL) {
0829:         string icon = "";
0830:         string info = "";
0831:         for (size_t i = 0; i < sizei++) {
0832:             if ((items[i].icon) == "" && (i > 25))    break;
0833:                                     //アイコンURLなく 'Z'を超えたら打ち止め
0834:             string mark = {(char)(65 + i)};
0835:             if (items[i].icon == "") {
0836:                 icon = "https://www.google.com/mapfiles/marker" + mark + ".png";
0837:             } else {
0838:                 icon = items[i].icon;
0839:             }
0840:             info = "";
0841:             if (items[i].description != L"") {
0842:                 snprintf(buffSIZE_BUFF, "', {maxWidth: %d, offset: [%d, %d] });", max_widthofst_xofst_y);
0843:                 info = "marker_" + mark + ".bindPopup('" + _WS(items[i].description) + buff;
0844:             }
0845:             snprintf(latSIZE_BUFF, "%.5f", items[i].latitude);
0846:             snprintf(lngSIZE_BUFF, "%.5f", items[i].longitude);
0847:             icode += (boost::format(R"(
0848:                 var icon_%1% =  new L.icon({
0849:                     iconUrl: '%2%',
0850:                     iconAnchor: [10, 10] //暫定
0851:                 });
0852:                 var marker_%1% = new L.Marker([%3%, %4%], {icon: icon_%1%}).addTo(map);
0853:                 %5%
0854: )
")
0855: mark          //マーカー識別子
0856: icon          //マーカーURL
0857: lat           //緯度
0858: lng           //経度
0859: info          //情報
0860: ).str();
0861:         }
0862:     }
0863: 
0864:     //地図描画スクリプトの生成
0865:     string script = (boost::format(R"(
0866: %6%
0867: <link rel=
"stylesheet" href="https://unpkg.com/leaflet@latest/dist/leaflet.css" />
0868: <script src=
"https://unpkg.com/leaflet@latest/dist/leaflet.js"></script>
0869: %7%
0870: <script>
0871: window.onload = function() {
0872:     var map = L.map('%1%',{zoomControl:false});
0873:     map.setView([%2%, %3%], %4%);
0874:     L.control.scale({
0875:         maxWidth: 250,
0876:         position: 'bottomright',
0877:         imperial: false
0878:     }).addTo(map);
0879:     L.control.zoom({position:'topleft'}).addTo(map);
0880: 
0881:     //地理院地図:標準地図
0882:     var GSISTD = new L.tileLayer(
0883:         'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
0884:         {
0885:             attribution: 
"<a href='https://maps.gsi.go.jp/development/ichiran.htmltarget='_blank'>地理院タイル</a>",
0886:             minZoom: 0,
0887:             maxZoom: 18,
0888:             name: 'GSISTD'
0889:         });
0890:     //地理院地図:淡色地図
0891:     var GSIPALE = new L.tileLayer(
0892:         'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
0893:         {
0894:             attribution: 
"<a href='https://maps.gsi.go.jp/development/ichiran.htmltarget='_blank'>地理院タイル</a>",
0895:             minZoom: 2,
0896:             maxZoom: 18,
0897:             name: 'GSIPALE'
0898:         });
0899:     //地理院地図:白地図
0900:     var GSIBLANK = new L.tileLayer(
0901:         'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
0902:         {
0903:             attribution: 
"<a href='https://maps.gsi.go.jp/development/ichiran.htmltarget='_blank'>地理院タイル</a>",
0904:             minZoom: 5,
0905:             maxZoom: 14,
0906:             name: 'GSIBLANK'
0907:         });
0908:     //地理院地図:写真
0909:     var GSIPHOTO = new L.tileLayer(
0910:         'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
0911:         {
0912:             attribution: 
"<a href='https://maps.gsi.go.jp/development/ichiran.htmltarget='_blank'>地理院タイル</a>",
0913:             minZoom: 2,
0914:             maxZoom: 18,
0915:             name: 'GSIPHOTO'
0916:         });
0917:     //OpenStreetMap
0918:     var OSM = new L.tileLayer(
0919:         'https://tile.openstreetmap.jp/{z}/{x}/{y}.png',
0920:         {
0921:             attribution: 
"<a href='https://osm.org/copyrighttarget='_blank'>OpenStreetMap</acontributors",
0922:             minZoom: 0,
0923:             maxZoom: 18,
0924:             name: 'OSM'
0925:         });
0926: %8%
0927: 
0928:     //baseMapsオブジェクトにタイル設定
0929:     var baseMaps = {
0930:         
"地理院地図" : GSISTD,
0931:         
"淡色地図" : GSIPALE,
0932:         
"白地図" : GSIBLANK,
0933:         
"写真地図" : GSIPHOTO,
0934:         
"オープンストリートマップ" : OSM
0935:         %9%
0936:     };
0937: 
0938:     //layersコントロールにbaseMapsオブジェクトを設定して地図に追加
0939:     L.control.layers(baseMaps).addTo(map);
0940:     %5%.addTo(map);
0941: 
0942:     //イベント追加
0943:     map.on('moveend', getPointData);
0944:     map.on('zoomend', getPointData);
0945:     map.on('baselayerchange', getPointData);
0946: 
0947:     //イベント発生時の地図情報を取得・格納
0948:     function getPointData() {
0949:         var pos = map.getCenter();
0950:         //経度
0951:         if (document.getElementById('longitude') != null) {
0952:             document.getElementById('longitude').value = pos.lng;
0953:         }
0954:         //緯度
0955:         if (document.getElementById('latitude') != null) {
0956:             document.getElementById('latitude').value = pos.lat;
0957:         }
0958:         //ズーム
0959:         if (document.getElementById('zoom') != null) {
0960:             document.getElementById('zoom').value = map.getZoom();
0961:         }
0962:         //タイプ
0963:         if (document.getElementById('maptype') != null) {
0964:             for (var k in baseMaps) {
0965:                 if (map.hasLayer(baseMaps[k])) {
0966:                     document.getElementById('maptype').value = baseMaps[k].options.name;
0967:                 }
0968:             }
0969:         }
0970:         %11%
0971:     }
0972:     %10%
0973:     %12%
0974: }
0975: </script>
0976: )
")
0977: id                    //地図ID
0978: latitude              //緯度
0979: longitude             //経度
0980: zoom                  //地図拡大率
0981: type                  //地図タイプ
0982: this->GoogleMap1       //Googleマップ描画用スクリプトURL
0983: this->GoogleMap2       //Leaftet:Googleマップ・アドオンURL
0984: this->GoogleMap3       //Leaftet:Googleマップ用レイヤ
0985: this->GoogleMap4       //Leaftet:Googleマップ選択肢
0986: icode
0987: call1
0988: call2
0989: ).str();
0990: 
0991:     return script;
0992: }

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

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

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

0093: /**
0094:  * AppDataのパスを取得
0095:  * @param char* appname アプリケーション名
0096:  * @return TCHAR* パス
0097:  */
0098: TCHARpahooGeocode::getMyPath(const charappname) {
0099:     static TCHAR myPath[MAX_PATH] = "";
0100: 
0101:     if (strlen(myPath) == 0) {
0102:         if (SHGetSpecialFolderPath(NULLmyPathCSIDL_APPDATA, 0)) {
0103:             TCHAR *ptmp = _tcsrchr(myPath_T('\\'));
0104:             if (ptmp != NULL) {
0105:                 ptmp = _tcsinc(ptmp);
0106:                 *ptmp = _T('\0');
0107:             }
0108:             strcat(myPath_T("Roaming"));
0109:             CreateDirectory((LPCTSTR)myPathNULL);
0110:             strcat(myPath_T("\\pahoo.org"));
0111:             CreateDirectory((LPCTSTR)myPathNULL);
0112:             strcat(myPath_T("\\"));
0113:             strcat(myPath_T(appname));
0114:             CreateDirectory((LPCTSTR)myPathNULL);
0115:             strcat(myPath_T("\\"));
0116:         } else {
0117:         }
0118:     }
0119:     return myPath;
0120: }

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

0179: /**
0180:  * Google Cloud Platform APIキーを書き込む
0181:  * @param string key  書き込むAPIキー
0182:  * @return bool TRUE:書込成功/FALSE:失敗
0183:  */
0184: bool pahooGeocode::writeGoogleApiKey(std::string key) {
0185:     bool ret = TRUE;
0186:     ofstream ofs;
0187: 
0188:     ofs.open((string)this->getMyPath(NULL) + FNAME_GOOGLE_API);
0189:     ofs << key;
0190:     if(ofs.bad()) {
0191:         this->errmsg = _SW("Google Cloud Platform APIキーの保存に失敗しました");
0192:         ret = FALSE;
0193:     }
0194:     ofs.close();
0195:     this->readGoogleApiKey();
0196: 
0197:     return ret;
0198: }

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

0123: /**
0124:  * Google Cloud Platform APIキーを読み込む
0125:  * @param なし
0126:  * @return bool TRUE:読込成功/FALSE:ファイルがない
0127:  */
0128: bool pahooGeocode::readGoogleApiKey(void) {
0129:     string key;
0130:     bool ret = FALSE;
0131: 
0132:     ifstream ifs((string)this->getMyPath(NULL) + this->FNAME_GOOGLE_API);
0133:     //APIキー・ファイルが無ければ初期化
0134:     if (!ifs) {
0135:         this->GoogleAPIkey = "";
0136:         this->GoogleMap1 = "";
0137:         this->GoogleMap2 = "";
0138:         this->GoogleMap3 = "";
0139:         this->GoogleMap4 = "";
0140:         return ret;
0141:     }
0142: 
0143:     //APIキー読み込み
0144:     ifs >> key;
0145:     if(ifs.bad()) {
0146:         this->errmsg = _SW("Google Cloud Platform APIキーの読み込みに失敗しました");
0147:         ifs.close();
0148:         this->GoogleAPIkey = "";
0149:         this->GoogleMap1 = "";
0150:         this->GoogleMap2 = "";
0151:         this->GoogleMap3 = "";
0152:         this->GoogleMap4 = "";
0153:         return FALSE;
0154:     }
0155:     ifs.close();
0156: 
0157:     //APIキーなどの設定
0158:     if (key.length() > 0) {
0159:         this->GoogleAPIkey = key;
0160:         this->GoogleMap1 = "<script src='https://maps.googleapis.com/maps/api/js?key="
0161:                 + key + "' async defer></script>";
0162:         this->GoogleMap2 =
0163:                 "<script src='https://unpkg.com/leaflet.gridlayer.googlemutant@0.10.2/Leaflet.GoogleMutant.js'></script>";
0164:         this->GoogleMap3 =
0165:                 "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'});";
0166:         this->GoogleMap4 = ",'Googleマップ(標準)' : GMRD,\n'Googleマップ(写真)' : GMST,\n'Googleマップ(混合)' : GMHB";
0167:         ret = TRUE;
0168:         //APIキーが無ければ初期化
0169:     } else {
0170:         this->GoogleAPIkey = "";
0171:         this->GoogleMap1 = "";
0172:         this->GoogleMap2 = "";
0173:         this->GoogleMap3 = "";
0174:         this->GoogleMap4 = "";
0175:     }
0176:     return ret;
0177: }

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

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

0613: /**
0614:  * 地図表示用HTML生成
0615:  * @param string 表示するインフォメーション
0616:  * @return string 生成したHTML文
0617:  */
0618: string makeMapHTML(wstring info) {
0619:     if (pGC->GoogleAPIkey.length() == 0) {
0620:         Maptype = DEF_MAPTYPE;
0621:     }
0622: 
0623:     int cnt = info2points();
0624:     string script = pGC->makeMapLeaflet(MAP_IDEarthquake[0].longitudeEarthquake[0].latitudeZoomMaptypepGC->Ppointscnt, "", "", INFO_WIDTHINFO_OFFSET_XINFO_OFFSET_Y);
0625: 
0626:     string html = (boost::format(R"(<!DOCTYPE html>
0627: <html lang=
"ja">
0628: <head>
0629: <meta charset=
"SJIS">
0630: <title>%9%</title>
0631: <meta name=
"author" content="studio pahoo" />
0632: <meta name=
"copyright" content="studio pahoo" />
0633: <meta name=
"ROBOTS" content="NOINDEX,NOFOLLOW" />
0634: <meta http-equiv=
"pragma" content="no-cache">
0635: <meta http-equiv=
"cache-control" content="no-cache">
0636: <meta http-equiv=
"X-UA-Compatible" content="IE=edge">
0637: %8%
0638: </head>
0639: <body>
0640: <div id=
"%1%" style="width:%2%pxheight:%3%px;"></div>
0641: <form>
0642: <input id=
"latitude"  type="hidden" value="%4%" />
0643: <input id=
"longitude" type="hidden" value="%5%" />
0644: <input id=
"zoom"      type="hidden" value="%6%" />
0645: <input id=
"maptype"   type="hidden" value="%7%" />
0646: </form>
0647: </body>
0648: </html>
0649: )
")
0650: MAP_ID                        //地図ID
0651: MAP_WIDTH                     //地図の幅
0652: MAP_HEIGHT                    //地図の高さ
0653: Earthquake[0].latitude       //緯度
0654: Earthquake[0].longitude      //経度
0655: Zoom                          //地図拡大率
0656: Maptype                       //地図タイプ
0657: script                        //地図描画スクリプト
0658: APPNAMEJP                     //アプリケーション名
0659: ).str();
0660: 
0661:     return html;
0662: }

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

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

0692: /**
0693:  * ブラウザ・コントロールを表示
0694:  * @param wstring info 情報ウィンドウに表示するテキスト
0695:  * @param char* tmpname 読み込むHTMLファイル名
0696:  * @return なし
0697: */
0698: void viewBrowser(wstring infoconst chartmpname) {
0699:     string html;
0700:     string fname;
0701:     ofstream ofs;
0702: 
0703:     //表示用HTMLファイル作成
0704:     if ((ErrorMessage == "") && (! pGC->isError())) {
0705:         html = makeMapHTML(info);
0706:     } else {
0707:         html = makeErrorHTML();
0708:     }
0709:     ofs.open(tmpname);
0710:     ofs << html;
0711:     ofs.close();
0712: 
0713:     fname = (string)tmpname;
0714:     std::replace(fname.begin(), fname.end(), '\\', '/');
0715:     wBrowser.loadPage((char*)fname.c_str());
0716:     wBrowser.fitToParent();
0717: }

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

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

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

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

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

0136: /**
0137:  * パラメータの初期化
0138:  * @param なし
0139:  * @return なし
0140:  */
0141: void initParameter(void) {
0142:     Longitude = DEF_LONGITUDE;
0143:     Latitude  = DEF_LATITUDE;
0144:     Zoom      = DEF_ZOOM;
0145:     Maptype   = DEF_MAPTYPE;
0146: }

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

0195: /**
0196:  * パラメータの保存
0197:  * @param なし
0198:  * @return なし
0199:  */
0200: void saveParameter(void) {
0201:     //ブラウザ・コントロールからパラメータを取り出す
0202:     wstring val;
0203:     if (wBrowser.getInputById(L"longitude", &val)) {
0204:         Longitude = stod(val);
0205:     }
0206:     if (wBrowser.getInputById(L"latitude", &val)) {
0207:         Latitude = stod(val);
0208:     }
0209:     if (wBrowser.getInputById(L"zoom", &val)) {
0210:         Zoom = stoi(val);
0211:     }
0212:     if (wBrowser.getInputById(L"maptype", &val)) {
0213:         Maptype = _WS(val);
0214:     }
0215: 
0216:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
0217:     snprintf(lngSIZE_BUFF, "%.5f", Longitude);
0218:     snprintf(latSIZE_BUFF, "%.5f", Latitude);
0219: 
0220:     //XMLファイルへ書き込む
0221:     ptree pt;
0222:     ptreechild1 = pt.add("parameter.param", lat);
0223:     child1.add("<xmlattr>.type", "latitude");
0224:     ptreechild2 = pt.add("parameter.param", lng);
0225:     child2.add("<xmlattr>.type", "longitude");
0226:     ptreechild3 = pt.add("parameter.param", to_string(Zoom));
0227:     child3.add("<xmlattr>.type", "zoom");
0228:     ptreechild4 = pt.add("parameter.param", Maptype);
0229:     child4.add("<xmlattr>.type", "maptype");
0230: 
0231:     const int indent = 4;
0232:     write_xml(getMyPath(APPNAME) + APPNAME + ".xml", ptstd::locale(),
0233:         xml_writer_make_settings<std::string>(' ', indent));
0234: }

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

0148: /**
0149:  * パラメータの読み込み
0150:  * @param なし
0151:  * @return なし
0152:  */
0153: void loadParameter(void) {
0154:     ptree pt;
0155: 
0156:     //初期値設定
0157:     initParameter();
0158: 
0159:     //XMLファイル読み込み
0160:     try {
0161:         xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
0162: 
0163:         //XML解釈
0164:         try {
0165:             //形式チェック
0166:             if (optional<string>str = pt.get_optional<string>("parameter")) {
0167:             } else {
0168:                 return;
0169:             }
0170:             //パラメータ読み込み
0171:             for (auto it : pt.get_child("parameter")) {
0172:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
0173:                 if (type == "latitude") {
0174:                     Latitude = stod(it.second.data());
0175:                 } else if (type == "longitude") {
0176:                     Longitude = stod(it.second.data());
0177:                 } else if (type == "zoom") {
0178:                     Zoom = stoi(it.second.data());
0179:                 } else if (type == "maptype") {
0180:                     Maptype = (string)it.second.data();
0181:                 }
0182:             }
0183:         //解釈失敗したら初期値設定
0184:         } catch (xml_parser_errore) {
0185:             initParameter();
0186:             return;
0187:         }
0188:     //読み込み失敗したら初期値設定
0189:     } catch (xml_parser_errore) {
0190:         initParameter();
0191:         return;
0192:     }
0193: }

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

参考サイト

(この項おわり)
header