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

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

(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ブラウザ・クラス(ヘッダ)

使用ライブラリ

気象庁地震情報サイトにアクセスするために、オープンソースのライブラリ 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" とする。

解説:定数など

0039: #define MAKER     "pahoo.org"                //作成者
0040: #define APPNAME     "earthquakewin"            //アプリケーション名
0041: #define APPNAMEJP "直近の地震情報"        //アプリケーション名日本語)
0042: #define APPVERSION "1.1"                    //バージョン
0043: #define APPYEAR     "2020"                    //作成年
0044: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-12-01.shtm"  // 参考サイト
0045: 
0046: //char*バッファサイズ
0047: #define SIZE_BUFF 512
0048: 
0049: //現在のインターフェイス
0050: HINSTANCE hInst;
0051: 
0052: //親ウィンドウ
0053: HWND hParent;
0054: 
0055: //エラー・メッセージ格納用
0056: string ErrorMessage;
0057: 
0058: //ヘルプ・ファイル
0059: #define HELPFILE ".\\etc\\help.chm"
0060: 
0061: //デフォルト保存ファイル名
0062: #define SAVEFILE "eq_%04d%02d%02d_%02d%02d.txt"
0063: 
0064: //ブラウザ・コントロール
0065: WebBrowser wBrowser;
0066: 
0067: //UserAgent
0068: string UserAgent;
0069: 
0070: //pahooGeocodeオブジェクト
0071: pahooGeocodepGC;
0072: 
0073: //マップID
0074: #define MAP_ID         "map_id"
0075: //地図の大きさ
0076: #define MAP_WIDTH     500        //地図の幅(ピクセル)
0077: #define MAP_HEIGHT     300        //地図の高さ(ピクセル)
0078: //経度・緯度(初期値)
0079: #define DEF_LONGITUDE 139.766667
0080: double Longitude = DEF_LONGITUDE;
0081: #define DEF_LATITUDE 35.681111
0082: double Latitude  = DEF_LATITUDE;
0083: //地図拡大率(初期値)
0084: #define DEF_ZOOM     6
0085: int Zoom = DEF_ZOOM;
0086: //地図の種類(初期値)
0087: #define DEF_MAPTYPE     "GSISTD"
0088: string Maptype = DEF_MAPTYPE;

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

解説:データ構造

0090: //地震情報
0091: wstring infoEarthquake;
0092: 
0093: //地震情報URL(気象庁):変更不可
0094: #define URL_EARTHQUAKE "https://www.jma.go.jp/jp/quake/"
0095: 
0096: //地震情報を格納する構造体
0097: struct _Earthquake {
0098:     int year            = 0;         //西暦年
0099:     int month           = 0;         //月
0100:     int day             = 0;         //日
0101:     int hour            = 0;         //時
0102:     int minuite         = 0;         //分
0103:     double latitude     = 0.0;            //緯度
0104:     double longitude    = 0.0;            //経度
0105:     double depth        = 0.0;            //深さ
0106:     double magnitude    = 0.0;            //マグニチュード
0107: Earthquake;

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

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

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 を使ってワイド文字列を総なめにしている。

解説:日本語テキスト変換

0249: /**
0250:  * 日本語テキスト変換(Win32 API利用)
0251:  * @param wstring sour   変換元テキスト
0252:  * @param DWORD   method 変換方式
0253:  * @return wstring 変換後テキスト
0254:  */
0255: wstring wconvString(wstring wsourDWORD method) {
0256:     //濁点・半濁点は全角→半角で2文字に増えるので変換後領域は余裕を持たせる
0257:     wchar_t wdest[wsour.length() * 2 + 1];
0258:     for (size_t i = 0; i < wsour.length() * 2 + 1; i++) {
0259:         wdest[i] = 0L;
0260:     }
0261:     //変換実行
0262:     LCMapStringW(LOCALE_SYSTEM_DEFAULTmethod, (LPWSTRwsour.c_str(),
0263:             wsour.length(), (LPWSTRwdestwsour.length() * 2);
0264: 
0265:     return (wstringwdest;
0266: }

PHP の組み込み関数  mb_convert_kana  に相当する機能をワイド文字列用に拡張したのが wconvString である。Win32API(Kernel32.dll)の機能を呼び出している。

解説:数字(wstring)を数値(double)に変換

0268: /**
0269:  * 数字(wstring)を数値(double)に変換
0270:  * @param wstring sour   変換元テキスト
0271:  * @return double  変換後数値
0272:  */
0273: double w2f(wstring sour) {
0274:     return atof(_WS(wconvString(sourLCMAP_HALFWIDTH)).c_str());
0275: }

気象庁地震情報サイトにある情報には、全角数字であるものが多く、コンピュータで処理しやすいように半角数字に変換するために、前述の関数 wconvString を利用した関数 w2f を用意した。

解説:Webコンテンツ取得

0446: /**
0447:  * cURLによるWebコンテンツ取得
0448:  * @param  string url アクセスURL
0449:  * @param  string ua  UserAgent
0450:  * @param  string *contents コンテンツを格納
0451:  * @return bool TRUE:読み込み成功/FALSE:失敗
0452:  */
0453: bool readWebContents(const string urlconst string uastring *contents) {
0454:     //cURLによる読み込み
0455:     bool ret = FALSE;
0456:     CURL *curl;
0457:     CURLcode res = (CURLcode)0;
0458:     curl = curl_easy_init();
0459:     if (curl) {
0460:         struct curl_slist *hs=NULL;
0461:         hs = curl_slist_append(hs, "Content-Type: application/x-www-form-urlencoded");
0462:         curl_easy_setopt(curlCURLOPT_URLurl.c_str());
0463:         curl_easy_setopt(curlCURLOPT_SSL_VERIFYPEER, 0);
0464:         curl_easy_setopt(curlCURLOPT_USERAGENTua.c_str());
0465:         curl_easy_setopt(curlCURLOPT_HTTPHEADERhs);
0466:         curl_easy_setopt(curlCURLOPT_WRITEFUNCTIONcallBackFuncCURL);
0467:         curl_easy_setopt(curlCURLOPT_WRITEDATAcontents);
0468:         res = curl_easy_perform(curl);
0469:         curl_easy_cleanup(curl);
0470:     }
0471:     //エラー・チェック
0472:     if (res == CURLE_OK)    ret = TRUE;
0473: 
0474:     return ret;
0475: }

Web 上のコンテンツを読み込むのに、cURL 関数群を使った関数 readWebContents を用意した。対象サイトが https でも読み込むことができる。

なお、アクセス時に HTTP ユーザーエージェントを指定できるようにしている。
HTTP ユーザーエージェントは、メインプログラムの花器で発生させている。
ヘルプの末尾にも記載したように、Web サイトに対して悪意のあるアクセスでないことを示す意味で、"Mozilla/5.0 ([アプリケーション名/バージョン/pahoo.org], Windows NT [バージョン])" を送信することを WeAPI管理者に申し送りしている。

0666:     //UserAgent生成
0667:     static OSVERSIONINFOEX os;
0668:     GetVersion2(&os);
0669:     UserAgent = (string)"Mozilla/5.0 (" + APPNAME + "/"
0670:         + APPVERSION + "/" + MAKER
0671:         + ", Windows NT " + to_string(os.dwMajorVersion) + "."
0672:         + to_string(os.dwMinorVersion) + ")";

解説:地震情報取得

0327: /**
0328:  * 地震情報取得(気象庁から)
0329:  * @param なし
0330:  * @return int (-1) : 取得失敗(ネットワーク接続異常)
0331:  *                0  : 情報無し
0332:  *              (+1) : 取得成功
0333: */
0334: int getEarthquake(void) {
0335:     string url = URL_EARTHQUAKE;
0336:     static string contents = "";
0337: 
0338:     //気象庁地震情報読み込み
0339:     bool res = readWebContents(urlUserAgent, &contents);
0340:     if (res == FALSE) {
0341:         ErrorMessage = "気象庁地震情報サイトへの接続エラーが発生しました";
0342:         return (-1);
0343:     }
0344: 
0345:     //コンテンツの解釈
0346:     setlocale(LC_ALL, "Japanese");
0347:     int ofst = 0;
0348:     stringstream ss;
0349:     string ss0;
0350:     wstring ws;
0351:     wsmatch mt1mt2;
0352:     wregex re1(_SW("震源・震度に関する情報(<br>)?(.+[0-9\\.0-9.]+と推定されます。)"));
0353:     wregex re2(_SW("(昭和|平成|令和)\\s*([0-90-9]+)年\\s*([0-90-9]+)月\\s*([0-90-9]+)日"));
0354:     wregex re3(_SW("([0-90-9]+)日([0-90-9]+)時([0-90-9]+)分ころ、地震がありました"));
0355:     wregex re4(_SW("北緯([0-9\\.0-9.]+)度、東経([0-9\\.0-9.]+)度"));
0356:     wregex re5(_SW("震源の深さは約([0-9\\.0-9.]+)km"));
0357:     wregex re6(_SW("地震の規模(マグニチュード)は([0-9\\.0-9.]+)と"));
0358: 
0359:     ss << contents;
0360:     while (ss && getline(ssss0)) {
0361:         //1行をwstring変換
0362:         ws = _UW(ss0);
0363:         if (regex_search(wsmt1re1)) {
0364:             wstring sour = mt1[2].str();
0365:             sour = wstrip_tags(sour);
0366:             if (sour == L"") {
0367:                 return 0;
0368:             }
0369:             //年月
0370:             if (regex_search(sourmt2re2)) {
0371:                 if (mt2[1].str() == _SW("昭和")) {
0372:                     ofst = 1925;
0373:                 } else if (mt2[1].str() == _SW("平成")) {
0374:                     ofst = 1988;
0375:                 } else if (mt2[1].str() == _SW("令和")) {
0376:                     ofst = 2018;
0377:                 }
0378:                 Earthquake.year  = (int)w2f(mt2[2].str()) + ofst;
0379:                 Earthquake.month = (int)w2f(mt2[3].str());
0380:             } else {
0381:                 return 0;
0382:             }
0383:             //日時分
0384:             if (regex_search(sourmt2re3)) {
0385:                 Earthquake.day     = (int)w2f(mt2[1].str());
0386:                 Earthquake.hour    = (int)w2f(mt2[2].str());
0387:                 Earthquake.minuite = (int)w2f(mt2[3].str());
0388:             } else {
0389:                 return 0;
0390:             }
0391:             //緯度・経度
0392:             if (regex_search(sourmt2re4)) {
0393:                 Earthquake.latitude  = w2f(mt2[1].str());
0394:                 Earthquake.longitude = w2f(mt2[2].str());
0395:             } else {
0396:                 return 0;
0397:             }
0398:             //深さ
0399:             if (regex_search(sourmt2re5)) {
0400:                 Earthquake.depth = w2f(mt2[1].str());
0401:             } else {
0402:                 return 0;
0403:             }
0404:             //マグニチュード
0405:             if (regex_search(sourmt2re6)) {
0406:                 Earthquake.magnitude = w2f(mt2[1].str());
0407:             } else {
0408:                 return 0;
0409:             }
0410:         }
0411:     }
0412: 
0413:     return 1;
0414: }

気象庁地震情報サイトからコンテンツを取り込むには、readWebContents で読み込んだコンテンツをスクレイピングしていく。今回も、ワイド文字列に対する正規表現を使うことにした。ソースは SJIS で書いているので、ユーザーマクロ関数 _SW を使ってワイド文字列に変換し、これを使って正規表現によるパターンマッチングを行う。

地震情報が記述されているのは 1行で、これにマッチするのが re1 である。あとは、この行に対してマッチングをかけていく。
スクレイピング用パターン
パターン名内容
re1地震情報が記述されている行
re2発表年月
re3発生日時分
re4震源の緯度・経度
re5震源の深さ
re6地震の規模

解説:マップを生成する

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

地図描画は "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生成

0429: /**
0430:  * 地図表示用HTML生成
0431:  * @param string 表示するインフォメーション
0432:  * @return string 生成したHTML文
0433:  */
0434: string makeMapHTML(wstring info) {
0435:     if (pGC->GoogleAPIkey.length() == 0) {
0436:         Maptype = DEF_MAPTYPE;
0437:     }
0438: 
0439:     //infoウィンドウの表示テキスト
0440:     pGC->Ppoints[0].latitude  = Earthquake.latitude;
0441:     pGC->Ppoints[0].longitude = Earthquake.longitude;
0442:     pGC->Ppoints[0].description =  wrepNL(info_SW("<br />"));
0443:     pGC->Ppoints[0].title = pGC->Ppoints[0].address = L"";
0444:     pGC->Ppoints[0].icon = "";
0445: 
0446:     string script = pGC->makeMapLeaflet(MAP_IDEarthquake.longitudeEarthquake.latitudeZoomMaptypepGC->Ppoints, 1);
0447:     string html = (boost::format(R"(<!DOCTYPE html>
0448: <html lang=
"ja">
0449: <head>
0450: <meta charset=
"SJIS">
0451: <title>フォトマップ</title>
0452: <meta name=
"author" content="studio pahoo" />
0453: <meta name=
"copyright" content="studio pahoo" />
0454: <meta name=
"ROBOTS" content="NOINDEX,NOFOLLOW" />
0455: <meta http-equiv=
"pragma" content="no-cache">
0456: <meta http-equiv=
"cache-control" content="no-cache">
0457: <meta http-equiv=
"X-UA-Compatible" content="IE=edge">
0458: %8%
0459: </head>
0460: <body>
0461: <div id=
"%1%" style="width:%2%pxheight:%3%px;"></div>
0462: <form>
0463: <input id=
"latitude"  type="hidden" value="%4%" />
0464: <input id=
"longitude" type="hidden" value="%5%" />
0465: <input id=
"zoom"      type="hidden" value="%6%" />
0466: <input id=
"maptype"   type="hidden" value="%7%" />
0467: </form>
0468: </body>
0469: </html>
0470: )
")
0471: MAP_ID                        //地図ID
0472: MAP_WIDTH                     //地図の幅
0473: MAP_HEIGHT                    //地図の高さ
0474: Earthquake.latitude            //緯度
0475: Earthquake.longitude           //経度
0476: Zoom                          //地図拡大率
0477: Maptype                       //地図タイプ
0478: script                        //地図描画スクリプト
0479: ).str();
0480: 
0481:     return html;
0482: }

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

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

0511: /**
0512:  * ブラウザ・コントロールを表示
0513:  * @param wstring info 情報ウィンドウに表示するテキスト
0514:  * @param char* tmpname 読み込むHTMLファイル名
0515:  * @return なし
0516: */
0517: void viewBrowser(wstring infoconst chartmpname) {
0518:     string html;
0519:     string fname;
0520:     ofstream ofs;
0521: 
0522:     //表示用HTMLファイル作成
0523:     if ((ErrorMessage == "") && (! pGC->isError())) {
0524:         html = makeMapHTML(info);
0525:     } else {
0526:         html = makeErrorHTML();
0527:     }
0528:     ofs.open(tmpname);
0529:     ofs << html;
0530:     ofs.close();
0531: 
0532:     fname = (string)tmpname;
0533:     std::replace(fname.begin(), fname.end(), '\\', '/');
0534:     wBrowser.loadPage((char*)fname.c_str());
0535:     wBrowser.fitToParent();
0536: }

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

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

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

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

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

0110: /**
0111:  * パラメータの初期化
0112:  * @param なし
0113:  * @return なし
0114:  */
0115: void initParameter(void) {
0116:     Longitude = DEF_LONGITUDE;
0117:     Latitude = DEF_LATITUDE;
0118:     Zoom = DEF_ZOOM;
0119:     Maptype = DEF_MAPTYPE;
0120: }

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

0169: /**
0170:  * パラメータの保存
0171:  * @param なし
0172:  * @return なし
0173:  */
0174: void saveParameter(void) {
0175:     //ブラウザ・コントロールからパラメータを取り出す
0176:     wstring val;
0177:     if (wBrowser.getInputById(L"longitude", &val)) {
0178:         Longitude = stod(val);
0179:     }
0180:     if (wBrowser.getInputById(L"latitude", &val)) {
0181:         Latitude = stod(val);
0182:     }
0183:     if (wBrowser.getInputById(L"zoom", &val)) {
0184:         Zoom = stoi(val);
0185:     }
0186:     if (wBrowser.getInputById(L"maptype", &val)) {
0187:         Maptype = _WS(val);
0188:     }
0189: 
0190:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
0191:     snprintf(lngSIZE_BUFF, "%.5f", Longitude);
0192:     snprintf(latSIZE_BUFF, "%.5f", Latitude);
0193: 
0194:     //XMLファイルへ書き込む
0195:     ptree pt;
0196:     ptreechild1 = pt.add("parameter.param", lat);
0197:     child1.add("<xmlattr>.type", "latitude");
0198:     ptreechild2 = pt.add("parameter.param", lng);
0199:     child2.add("<xmlattr>.type", "longitude");
0200:     ptreechild3 = pt.add("parameter.param", to_string(Zoom));
0201:     child3.add("<xmlattr>.type", "zoom");
0202:     ptreechild4 = pt.add("parameter.param", Maptype);
0203:     child4.add("<xmlattr>.type", "maptype");
0204: 
0205:     const int indent = 4;
0206:     write_xml(getMyPath(APPNAME) + APPNAME + ".xml", ptstd::locale(),
0207:         xml_writer_make_settings<std::string>(' ', indent));
0208: }

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

0122: /**
0123:  * パラメータの読み込み
0124:  * @param なし
0125:  * @return なし
0126:  */
0127: void loadParameter(void) {
0128:     ptree pt;
0129: 
0130:     //初期値設定
0131:     initParameter();
0132: 
0133:     //XMLファイル読み込み
0134:     try {
0135:         xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
0136: 
0137:         //XML解釈
0138:         try {
0139:             //形式チェック
0140:             if (optional<string>str = pt.get_optional<string>("parameter")) {
0141:             } else {
0142:                 return;
0143:             }
0144:             //パラメータ読み込み
0145:             for (auto it : pt.get_child("parameter")) {
0146:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
0147:                 if (type == "latitude") {
0148:                     Latitude = stod(it.second.data());
0149:                 } else if (type == "longitude") {
0150:                     Longitude = stod(it.second.data());
0151:                 } else if (type == "zoom") {
0152:                     Zoom = stoi(it.second.data());
0153:                 } else if (type == "maptype") {
0154:                     Maptype = (string)it.second.data();
0155:                 }
0156:             }
0157:         //解釈失敗したら初期値設定
0158:         } catch (xml_parser_errore) {
0159:             initParameter();
0160:             return;
0161:         }
0162:     //読み込み失敗したら初期値設定
0163:     } catch (xml_parser_errore) {
0164:         initParameter();
0165:         return;
0166:     }
0167: }

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

参考サイト

(この項おわり)
header