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

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

(2022年11月12日)使用ライブラリをバージョンアップ
(2022年9月3日)各種パラメータの保存に対応,最新版ライブラリに更新.
(2022年3月10日)気象庁防災情報XMLのhttps化に対応。新しいプログラムを実行するとき、一度だけ「キャッシュ・クリア」を実行するといいだろう。

目次

サンプル・プログラム

圧縮ファイルの内容
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ビルド

使用ライブラリ

気象庁防災情報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" を利用してほしい。

解説:定数など

0040: #define MAKER     "pahoo.org"               //作成者
0041: #define APPNAME       "earthquakewin"           //アプリケーション名
0042: #define APPNAMEJP "最近の地震情報"     //アプリケーション名(日本語)
0043: #define APPVERSION    "3.4.1"                   //バージョン
0044: #define APPYEAR       "2020-22"             //作成年
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: unsigned hParent_XhParent_Y;
0058: 
0059: //エラー・メッセージ格納用【変更不可】
0060: string ErrorMessage;
0061: 
0062: //ListViewItemの最大文字長【変更不可】
0063: #define MAX_LISTVIEWITEM  259
0064: 
0065: //ヘルプ・ファイル
0066: #define HELPFILE  ".\\etc\\help.chm"
0067: 
0068: //デフォルト保存ファイル名
0069: #define SAVEFILE  "eq_%04d%02d%02d_%02d%02d.csv"

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

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

解説:データ構造

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

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

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

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

0071: //キャッシュ・ディレクトリ
0072: #define DIR_CACHE_FEED        "pcache1\\"
0073: #define DIR_CACHE_FEED_L  "pcache2\\"
0074: #define DIR_CACHE_DATA        "pcache3\\"
0075: 
0076: //キャッシュ保持時間(デフォルト;分)(0:キャッシュしない)
0077: #define LIFE_CACHE_FEED       5       //高頻度フィードに対して
0078: #define LIFE_CACHE_FEED_L 120     //長期フィードに対して
0079: #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 を使ってワイド文字列を総なめにしている。

解説:地震情報取得

0489: /**
0490:  * 地震情報取得(気象庁防災情報XMLから)
0491:  * @param  なし
0492:  * @return bool TRUE:取得成功/FALSE:失敗
0493: */
0494: bool getEarthquake(void) {
0495:    //年月日時分
0496:    regex re1("([0-9]+)\\-([0-9]+)\\-([0-9]+)T([0-9]+)\\:([0-9]+)");
0497:    //緯度・経度・深さ
0498:    regex re2("([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\-\\+\\/])([0-9]*)");
0499:    smatch mt1mt2;
0500:    double lnglat;
0501:    pahooCachepCC; //pahooCacheオブジェクト
0502: 
0503:    //地震情報URLを取得
0504:    static string urls[MAX_EARTHQUAKE];
0505:    bool res = jma_getLastEarthquakeURLs(urls);
0506:    if (res == FALSE) {
0507:        return FALSE;
0508:    }
0509: 
0510:    static string contents = "";
0511:    pCC = new pahooCache(LIFE_CACHE_DATAgetMyPath(APPNAME) + DIR_CACHE_DATAUserAgent);
0512:    for (int i = 0; i < MAX_EARTHQUAKEi++) {
0513:        if (urls[i] == "") {
0514: //         cout << "count = " << i << endl;
0515:            break;
0516:        }
0517: //     readWebContents(urls[i], UserAgent, &contents);
0518:        if (pCC->load(urls[i], &contents) == FALSE) {
0519:            ErrorMessage = _WS(pCC->getError());
0520:            return FALSE;
0521:        }
0522:        //XML読み込み
0523:        try {
0524:            std::stringstream ss;
0525:            ss << contents;
0526:            ptree pt;
0527:            xml_parser::read_xml(sspt);
0528: 
0529:            //XML解釈
0530:            //地震発生日時の取得
0531:            if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.OriginTime")) {
0532: //             cout << str.value() << " ";
0533:                if (regex_search(str.value(), mt1re1)) {
0534:                    Earthquake[i].year    = stoi(mt1[1].str());
0535:                    Earthquake[i].month   = stoi(mt1[2].str());
0536:                    Earthquake[i].day     = stoi(mt1[3].str());
0537:                    Earthquake[i].hour    = stoi(mt1[4].str());
0538:                    Earthquake[i].minuite = stoi(mt1[5].str());
0539:                }
0540:            }
0541:            //震源地の取得
0542:            if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.Name")) {
0543:                Earthquake[i].location = _UW(str.value());
0544: //             cout << _WS(Earthquake[i].location) << "  ";
0545:            }
0546:            if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.Hypocenter.Area.jmx_eb:Coordinate")) {
0547:                if (regex_search(str.value(), mt2re2)) {
0548:                    pGC->tokyo_wgs84(stod(mt2[2].str()), stod(mt2[1].str()), &lng, &lat);
0549:                    Earthquake[i].latitude  = lat;
0550:                    Earthquake[i].longitude = lng;
0551:                    if (mt2[3].str() == "/") {
0552:                        Earthquake[i].depth = (-1.0);
0553:                    } else {
0554:                        Earthquake[i].depth = stod(mt2[4].str());
0555:                    }
0556: //                 cout << "lat=" << lat << " lng=" << lng << " ";
0557:                }
0558:            }
0559:            //マグニチュードの取得
0560:            if (optional<string>str = pt.get_optional<string>("Report.Body.Earthquake.jmx_eb:Magnitude")) {
0561:                Earthquake[i].magnitude = stod(str.value());
0562: //             cout << "M=" << Earthquake[i].magnitude << " ";
0563:            }
0564:            //最大震度の取得
0565:            if (optional<string>str = pt.get_optional<string>("Report.Body.Intensity.Observation.MaxInt")) {
0566:                Earthquake[i].maxintensity = stoi(str.value());
0567: //             cout << "max=" << Earthquake[i].maxintensity << endl;
0568:            }
0569:        //読み込みエラー
0570:        } catch(xml_parser_errore) {
0571:            ErrorMessage = "気象庁防災情報XMLにアクセスできません";
0572:            return FALSE;
0573:        }
0574:        contents.clear();
0575:    }
0576:    delete pCC;
0577: 
0578:    return TRUE;
0579: }

気象庁防災情報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生成

0717: /**
0718:  * 地図表示用HTML生成
0719:  * @param  string 表示するインフォメーション
0720:  * @return string 生成したHTML文
0721:  */
0722: string makeMapHTML(wstring info) {
0723: // if (pGC->GoogleAPIkey.length() == 0) {
0724: //     Maptype = DEF_MAPTYPE;
0725: // }
0726: 
0727:    int cnt = info2points();
0728:    string script = pGC->makeMapLeaflet(MAP_IDEarthquake[0].longitudeEarthquake[0].latitudeZoomMaptypepGC->Ppointscnt, "", "", INFO_WIDTHINFO_OFFSET_XINFO_OFFSET_Y);
0729: 
0730:    string html = (boost::format(R"(<!DOCTYPE html>
0731: <html lang=
"ja">
0732: <head>
0733: <meta charset=
"SJIS">
0734: <title>%9%</title>
0735: <meta name=
"author" content="studio pahoo" />
0736: <meta name=
"copyright" content="studio pahoo" />
0737: <meta name=
"ROBOTS" content="NOINDEX,NOFOLLOW" />
0738: <meta http-equiv=
"pragma" content="no-cache">
0739: <meta http-equiv=
"cache-control" content="no-cache">
0740: <meta http-equiv=
"X-UA-Compatible" content="IE=edge">
0741: %8%
0742: </head>
0743: <body>
0744: <div id=
"%1%" style="width:%2%pxheight:%3%px;"></div>
0745: <form>
0746: <input id=
"latitude"  type="hidden" value="%4%" />
0747: <input id=
"longitude" type="hidden" value="%5%" />
0748: <input id=
"zoom"      type="hidden" value="%6%" />
0749: <input id=
"maptype"   type="hidden" value="%7%" />
0750: </form>
0751: </body>
0752: </html>
0753: )
")
0754: MAP_ID                       //地図ID
0755: MAP_WIDTH                        //地図の幅
0756: MAP_HEIGHT                   //地図の高さ
0757: Earthquake[0].latitude      //緯度
0758: Earthquake[0].longitude     //経度
0759: Zoom                         //地図拡大率
0760: Maptype                      //地図タイプ
0761: script                       //地図描画スクリプト
0762: APPNAMEJP                        //アプリケーション名
0763: ).str();
0764: 
0765:    return html;
0766: }

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

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

0796: /**
0797:  * ブラウザ・コントロールを表示
0798:  * @param  wstring info 情報ウィンドウに表示するテキスト
0799:  * @param  char* tmpname 読み込むHTMLファイル名
0800:  * @return なし
0801: */
0802: void viewBrowser(wstring infoconst chartmpname) {
0803:    string html;
0804:    string fname;
0805:    ofstream ofs;
0806: 
0807:    //表示用HTMLファイル作成
0808:    if ((ErrorMessage == "") && (! pGC->isError())) {
0809:        html = makeMapHTML(info);
0810:    } else {
0811:        html = makeErrorHTML();
0812:    }
0813:    ofs.open(tmpname);
0814:    ofs << html;
0815:    ofs.close();
0816: 
0817:    fname = (string)tmpname;
0818:    std::replace(fname.begin(), fname.end(), '\\', '/');
0819:    wBrowser.loadPage((char*)fname.c_str());
0820:    wBrowser.fitToParent();
0821: }

ユーザー関数 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, hDlgNULLhInstNULL);
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 のグローバル変数に、それぞれ代入している。

0139: /**
0140:  * パラメータの初期化
0141:  * @param  なし
0142:  * @return なし
0143:  */
0144: void initParameter(void) {
0145:    Longitude = DEF_LONGITUDE;
0146:    Latitude  = DEF_LATITUDE;
0147:    Zoom      = DEF_ZOOM;
0148:    Maptype   = DEF_MAPTYPE;
0149:    hParent_X = 0;
0150:    hParent_Y = 0;
0151: }

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

0216: /**
0217:  * パラメータの保存
0218:  * @param  なし
0219:  * @return なし
0220:  */
0221: void saveParameter(void) {
0222: #ifndef CMDAPP
0223:    //アプリケーション・ウィンドウの位置取得
0224:    WINDOWINFO windowInfo;
0225:    windowInfo.cbSize = sizeof(WINDOWINFO);
0226:    GetWindowInfo(hParent, &windowInfo);
0227:    hParent_X = (unsigned)windowInfo.rcWindow.left;
0228:    hParent_Y = (unsigned)windowInfo.rcWindow.top;
0229:    if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
0230:        hParent_X = 0;
0231:    }
0232:    if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
0233:        hParent_Y = 0;
0234:    }
0235: #endif
0236: 
0237:    //ブラウザ・コントロールからパラメータを取り出す
0238:    wstring val;
0239:    if (wBrowser.getInputById(L"longitude", &val)) {
0240:        Longitude = stod(val);
0241:    }
0242:    if (wBrowser.getInputById(L"latitude", &val)) {
0243:        Latitude = stod(val);
0244:    }
0245:    if (wBrowser.getInputById(L"zoom", &val)) {
0246:        Zoom = stoi(val);
0247:    }
0248:    if (wBrowser.getInputById(L"maptype", &val)) {
0249:        Maptype = _WS(val);
0250:    }
0251: 
0252:    char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
0253:    snprintf(lngSIZE_BUFF, "%.5f", Longitude);
0254:    snprintf(latSIZE_BUFF, "%.5f", Latitude);
0255: 
0256:    //XMLファイルへ書き込む
0257:    ptree pt;
0258:    ptreechild1 = pt.add("parameter.param", lat);
0259:    child1.add("<xmlattr>.type", "latitude");
0260:    ptreechild2 = pt.add("parameter.param", lng);
0261:    child2.add("<xmlattr>.type", "longitude");
0262:    ptreechild3 = pt.add("parameter.param", to_string(Zoom));
0263:    child3.add("<xmlattr>.type", "zoom");
0264:    ptreechild4 = pt.add("parameter.param", Maptype);
0265:    child4.add("<xmlattr>.type", "maptype");
0266:    ptreechild5 = pt.add("parameter.param", (string)to_string(hParent_X));
0267:    child5.add("<xmlattr>.type", "wx");
0268:    ptreechild6 = pt.add("parameter.param", (string)to_string(hParent_Y));
0269:    child6.add("<xmlattr>.type", "wy");
0270: 
0271:    const int indent = 4;
0272:    write_xml(getMyPath(APPNAME) + APPNAME + ".xml", ptstd::locale(),
0273:        xml_writer_make_settings<std::string>(' ', indent));
0274: }

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

0153: /**
0154:  * パラメータの読み込み
0155:  * @param  なし
0156:  * @return なし
0157:  */
0158: void loadParameter(void) {
0159:    ptree pt;
0160: 
0161:    //初期値設定
0162:    initParameter();
0163: 
0164:    //XMLファイル読み込み
0165:    try {
0166:        xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
0167: 
0168:        //XML解釈
0169:        try {
0170:            //形式チェック
0171:            if (optional<string>str = pt.get_optional<string>("parameter")) {
0172:            } else {
0173:                return;
0174:            }
0175:            //パラメータ読み込み
0176:            for (auto it : pt.get_child("parameter")) {
0177:                string typeit.second.get_optional<string>("<xmlattr>.type").value();
0178:                if (type == "latitude") {
0179:                    Latitude = stod(it.second.data());
0180:                } else if (type == "longitude") {
0181:                    Longitude = stod(it.second.data());
0182:                } else if (type == "zoom") {
0183:                    Zoom = stoi(it.second.data());
0184:                } else if (type == "maptype") {
0185:                    Maptype = (string)it.second.data();
0186:                } else if (type == "wx") {
0187:                    hParent_X = (unsigned)stoi(it.second.data());
0188:                } else if (type == "wy") {
0189:                    hParent_Y = (unsigned)stoi(it.second.data());
0190:                }
0191:            }
0192:        //解釈失敗したら初期値設定
0193:        } catch (xml_parser_errore) {
0194:            initParameter();
0195:            return;
0196:        }
0197:    //読み込み失敗したら初期値設定
0198:    } catch (xml_parser_errore) {
0199:        initParameter();
0200:        return;
0201:    }
0202: 
0203:    //アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
0204:    HWND hDesktop = GetDesktopWindow();
0205:    WINDOWINFO windowInfo;
0206:    windowInfo.cbSize = sizeof(WINDOWINFO);
0207:    GetWindowInfo(hDesktop, &windowInfo);
0208:    if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
0209:        hParent_X = 0;
0210:    }
0211:    if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
0212:        hParent_Y = 0;
0213:    }
0214: }

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

参考サイト

(この項おわり)
header