C++ で最寄駅を検索

(1/1)
>C++で最寄駅を検索
インターネット経由で HeartRails Express API - 最寄駅情報取得 API にアクセスし、地図や住所、ランドマーク、郵便番号等から最寄駅を検索し、距離順に一覧表示する。一覧情報をクリップボードにコピーしたり、CSV 形式ファイルに保存することができる。また、ユーザーが API キーを取得することで Google マップや Google 住所検索も利用できるようになる。
PHP で住所・ランドマークから最寄駅を求める」で作った PHP プログラムを C++に移植したものである。

目次

サンプル・プログラム

圧縮ファイルの内容
stationsearchwin.msiインストーラ
bin/stationsearchwin.exe実行プログラム本体
bin/cwebpage.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/stationsearchwin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ソース)
sour/pahooGeocode.hpp住所・緯度・経度に関わるクラス(ヘッダ)
sour/apikey.cppAPIキーの管理(ソース)
sour/apikey.hppAPIキーの管理(ヘッダ)
sour/webbrowser.hppWebブラウザ・クラス(ヘッダ)

使用ライブラリ

WebAPI にアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。

リソースの準備

今回は 32 ビット版の開発環境を用いる。
Eclipse を起動し、新規プロジェクト stationsearchwin を用意する。
ResEdit を起動し、resource.rc を用意する。

Eclipse に戻り、ソース・プログラム "stationsearchwin.cpp" を追加する。
リンカー・フラグを -Wl,--enable-stdcall-fixup -mwindows -lgdiplus -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl "(任意のパス)\libcurl.dll" "(任意のパス)\cwebpage.dll" "C:\Windows\system32\GdiPlus.dll" に設定する。
また、コマンド行パターンを ${COMMAND} ${FLAGS} ${OUTPUT_FLAG} ${OUTPUT_PREFIX}${OUTPUT} ${INPUTS} -lole32 -loleaut32 -luuid にすること。

解説:定数など

0036: // 定数など ==================================================================
0037: #define APPNAME     "stationsearchwin"    //アプリケーション名
0038: #define APPNAMEJP "最寄駅検索"        //アプリケーション名(日本語)
0039: #define APPVERSION "1.0"                //バージョン
0040: #define APPYEAR     "2020"                //作成年
0041: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-15-01.shtm"  //参考サイト
0042: 
0043: //ListViewItemの最大文字長:変更不可
0044: #define MAX_LISTVIEWITEM 259
0045: 
0046: //ヘルプ・ファイル
0047: #define HELPFILE ".\\etc\\help.chm"
0048: 
0049: //デフォルト保存ファイル名
0050: #define SAVEFILE "stationsearchwin.csv"
0051: 
0052: //エラー・メッセージ格納用:変更不可
0053: string ErrorMessage;
0054: 
0055: //現在のインターフェイス
0056: HINSTANCE hInst;
0057: 
0058: //親ウィンドウ
0059: HWND hParent;
0060: 
0061: //ブラウザ・コントロール
0062: WebBrowser wBrowser;
0063: 
0064: //pahooGeocodeオブジェクト
0065: pahooGeocodepGC;
0066: 
0067: //マップID
0068: #define MAP_ID         "map_id"
0069: //地図の大きさ
0070: #define MAP_WIDTH     530        //地図の幅(ピクセル)
0071: #define MAP_HEIGHT     300        //地図の高さ(ピクセル)
0072: //経度・緯度(初期値)
0073: #define DEF_LONGITUDE 139.766667
0074: double Longitude = DEF_LONGITUDE;
0075: #define DEF_LATITUDE 35.681111
0076: double Latitude  = DEF_LATITUDE;
0077: //地図拡大率(初期値)
0078: #define DEF_ZOOM 13
0079: int Zoom = DEF_ZOOM;
0080: //地図の種類(初期値)
0081: #define DEF_MAPTYPE     "GSISTD"
0082: string Maptype = DEF_MAPTYPE;

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

解説:検索の流れ

本アプリケーションの肝は、検索ボタンをクリックしたときのフローである。
>C++で最寄駅を検索

0826:         //検索
0827:         case IDM_EXEC:
0828:         case IDC_BUTTON_EXEC:
0829:             //検索キーがあればジオコードAPI呼び出し
0830:             query = _SW(getStrEditBox(hDlgIDC_EDIT_QUERY));
0831:             if (query.length() > 0) {
0832:                 if (pGC->searchPoints(query, 0) > 0) {
0833:                     Longitude = pGC->Ppoints[0].longitude;
0834:                     Latitude  = pGC->Ppoints[0].latitude;
0835:                 }
0836:             //検索キーがなければ地図中心座標を取り出す
0837:             } else {
0838:                 if (wBrowser.getInputById(L"longitude", &val)) {
0839:                     Longitude = stod(val);
0840:                 }
0841:                 if (wBrowser.getInputById(L"latitude", &val)) {
0842:                     Latitude = stod(val);
0843:                 }
0844:             }
0845:             if (wBrowser.getInputById(L"zoom", &val)) {
0846:                 Zoom = stoi(val);
0847:             }
0848:             if (wBrowser.getInputById(L"maptype", &val)) {
0849:                 Maptype = _WS(val);
0850:             }
0851:             //カーソルを砂時計に
0852:             SetCursor(LoadCursor(NULLIDC_WAIT));
0853:             //最寄駅検索
0854:             cnt = pHR->getResults_Heartrails(LatitudeLongitude);
0855:             if (cnt > 0) {
0856:                 cnt = makeInformation(cnt);
0857:             }
0858:             //ブラウザ・コントロールに表示
0859:             viewBrowser(tmpnamecnt);
0860:             //最寄駅の一覧表示
0861:             makeListView(GetDlgItem(hDlgIDC_LISTVIEW_STATIONS));
0862:             //エラー・リセット
0863:             ErrorMessage = "";
0864:             pGC->resetError();
0865:             break;

解説:INPUT要素の取得

以前に作った makeMapLeaflet メソッドにより、マップをドラッグしたり、表示倍率を変更したり、マップタイプを変更した結果は、ブラウザ・コントロールの input フォームにリアルタイムで反映される。
JavaScript であれば getElementById メソッドを使って取り出せるのだが、C++で同じことをやるのに骨が折れた。

0191: /**
0192:  * Webブラウザ・コントロールにある要素を取り出す(ID指定)
0193:  * @param wstring id ID
0194:  * @param IHTMLElement** element 要素を格納
0195:  * @return bool TRUE:要素があった/なかった
0196: */
0197: bool getIHTMLElementById(std::wstring idIHTMLElement** pElement) {
0198:     HWND iES = getIES();
0199:     static const UINT WM_HTML_GETOBJECT = ::RegisterWindowMessage(_T("WM_HTML_GETOBJECT"));
0200:     LRESULT res = 0;
0201:     SendMessageTimeout(iESWM_HTML_GETOBJECT, 0, 0, SMTO_ABORTIFHUNG, 1000, reinterpret_cast<PDWORD_PTR>(&res));
0202:     if (! res) {
0203:         return FALSE;
0204:     }
0205: 
0206:     //DLL 内のエクスポート関数のアドレスを取得
0207:     HINSTANCE hInstance = LoadLibrary(_T("oleacc.dll"));
0208:     LPFNOBJECTFROMLRESULT pfObjectFromLresult = (LPFNOBJECTFROMLRESULT)::GetProcAddress(hInstance, "ObjectFromLresult" );
0209:     if (pfObjectFromLresult == NULL) {
0210:         return FALSE;
0211:     }
0212: 
0213:      //IHTMLDocument3 のポインタを取得
0214:     IHTMLDocument3pHTMLDocument3;
0215:     (*pfObjectFromLresult)(resIID_IHTMLDocument3, 0, (void **)&pHTMLDocument3);
0216:     if (! pHTMLDocument3) {
0217:         return FALSE;
0218:     }
0219: 
0220:      //IHTMLElement のポインタを取得
0221:     BSTR bstrId;
0222:     IHTMLElementelement;
0223:     bstrId = SysAllocString(id.c_str());
0224:     pHTMLDocument3->getElementById(bstrId, &element);
0225: 
0226:     *pElement = element;
0227:     if (! element) {
0228:         return FALSE;
0229:     }
0230:     return TRUE;
0231: }

今回は COM インターフェースを使って取り出しているのだが、IHTMLDocument3 ポインタを取得するのに、ブラウザ・コントロールの奥深くにある IES(InternetExplorer_Server)のハンドルを知らなければならない。
>C++で最寄駅を検索
WinExplorer v1.30 を使って IES の位置を見たところ。かなり深いところにある。

0090: /**
0091:  * IES(InternetExplorer_Server)のハンドルを返す
0092:  * @param なし
0093:  * @return HWND ハンドル
0094: */
0095: HWND getIES(void) {
0096:     TCHAR className[128];
0097:     HWND hChild = GetWindow(controlGW_CHILD);
0098:     while (hChild) {
0099:         GetClassName(hChildclassNamesizeof(className));
0100:         if (_tcscmp(_T("Internet Explorer_Server"), className) == 0) {
0101:             return hChild;
0102:         }
0103:         hChild = GetWindow(hChildGW_CHILD);
0104:     }
0105:     return NULL;
0106: }

関数 getIES を用意し、IES を取り出すことができるようにした。

HeartRails Geo API 最寄駅検索

緯度・経度から最寄駅検索するのに、「HeartRails Express API - 最寄駅情報取得 API 」を利用する。
WebAPIのURL
URL
http://express.heartrails.com/api/xml

入力パラメータ
フィールド名 要否 内  容
method 必須 メソッド名:getStation(固定)
x 必須 最寄駅を取得したい場所の経度(世界測地系)。
y 必須 最寄駅を取得したい場所の緯度(世界測地系)。
応答データ(xml) response station name 駅名 line 路線名 distance 検索地点からの距離 x 経度 y 緯度 prefecture 都道府県 postal 郵便番号 next 次の駅 prev 前の駅

解説:HeartRails Geo APIの呼び出し

0279: /**
0280:  * HeartRails Express API から必要な情報を配列に格納する
0281:  * @param double latitude  緯度(世界測地系)
0282:  * @param double longitude 経度(世界測地系)
0283:  * @return int ヒット数/(-1):エラー発生
0284: */
0285: int pahooHeartRailsExpress::getResults_Heartrails(double latitudedouble longitude) {
0286:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
0287: 
0288:     snprintf(lngSIZE_BUFF, "%.5f", longitude);
0289:     snprintf(latSIZE_BUFF, "%.5f", latitude);
0290:     this->webapi = "http://express.heartrails.com/api/xml?method=getStations&x=" + (string)lng + "&y=" + (string)lat;
0291:     static string contents = "";
0292:     bool res = readWebContents(this->webapi, &contents);
0293:     if (res == FALSE) {
0294:         this->errmsg = _SW("HeartRails Express APIの接続エラーが発生しました");
0295:         return (-1);
0296:     }
0297: 
0298:     //配列の初期化
0299:     for (int i = 0; i < __SIZE_PPOINTSi++) {
0300:         this->Pstations[i].id = 0;
0301:         this->Pstations[i].title = this->Pstations[i].line = this->Pstations[i].distance = L"";
0302:         this->Pstations[i].latitude = this->Pstations[i].longitude = 0.0;
0303:     }
0304: 
0305:     //XML読み込み
0306:     int cnt = 0;
0307:     try {
0308:         std::stringstream ss;
0309:         ss << contents;
0310:         ptree pt;
0311:         xml_parser::read_xml(sspt);
0312: 
0313:         //応答チェック
0314:         if (optional<string>str = pt.get_optional<string>("response.station")) {
0315:         } else {
0316:             this->errmsg = _SW("HeartRails Geo APIの応答エラーが発生しました");
0317:             return (-1);
0318:         }
0319: 
0320:         //XML解釈
0321:         try {
0322:             for (auto it : pt.get_child("response")) {
0323:                 if (cnt >= __SIZE_PPOINTS) {
0324:                     break;
0325:                 }
0326:                 //読み飛ばし
0327:                 if (it.first != "station") {
0328:                     continue;
0329:                 }
0330:                 //最寄駅名
0331:                 if (optional<string>name = it.second.get_optional<string>("name")) {
0332:                     this->Pstations[cnt].title = _UW(name.value());
0333:                 }
0334:                 //緯度
0335:                 if (optional<string>lat = it.second.get_optional<string>("y")) {
0336:                     this->Pstations[cnt].latitude = stod(lat.value());
0337:                 }
0338:                 //経度
0339:                 if (optional<string>lng = it.second.get_optional<string>("x")) {
0340:                     this->Pstations[cnt].longitude = stod(lng.value());
0341:                 }
0342:                 //路線
0343:                 if (optional<string>line = it.second.get_optional<string>("line")) {
0344:                     this->Pstations[cnt].line = _UW(line.value());
0345:                 }
0346:                 //距離
0347:                 if (optional<string>distance = it.second.get_optional<string>("distance")) {
0348:                     this->Pstations[cnt].distance = _UW(distance.value());
0349:                 }
0350:                 //識別子
0351:                 this->Pstations[cnt].id = {(char)(65 + cnt)};
0352:                 cnt++;
0353:             }
0354:         //XML解釈エラー
0355:         } catch(ptree_bad_pathe) {
0356:             this->errmsg = _SW("HeartRails Geo APIの検索エラーが発生しました");
0357:             return (-1);
0358:         }
0359: 
0360:     //読み込みエラー
0361:     } catch(xml_parser_errore) {
0362:         this->errmsg = _SW("HeartRails Geo APIの接続エラーが発生しました");
0363:         return (-1);
0364:     }
0365:     contents.clear();
0366: 
0367:     return cnt;
0368: }

解説:パラメータの保存と読み込み

0097: /**
0098:  * パラメータの読み込み
0099:  * @param なし
0100:  * @return なし
0101:  */
0102: void loadParameter(void) {
0103:     ptree pt;
0104: 
0105:     //初期値設定
0106:     initParameter();
0107: 
0108:     //XMLファイル読み込み
0109:     try {
0110:         xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
0111: 
0112:         //XML解釈
0113:         try {
0114:             //形式チェック
0115:             if (optional<string>str = pt.get_optional<string>("parameter")) {
0116:             } else {
0117:                 return;
0118:             }
0119:             //パラメータ読み込み
0120:             for (auto it : pt.get_child("parameter")) {
0121:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
0122:                 if (type == "latitude") {
0123:                     Latitude = stod(it.second.data());
0124:                 } else if (type == "longitude") {
0125:                     Longitude = stod(it.second.data());
0126:                 } else if (type == "zoom") {
0127:                     Zoom = stoi(it.second.data());
0128:                 } else if (type == "maptype") {
0129:                     Maptype = (string)it.second.data();
0130:                 }
0131:             }
0132:         //解釈失敗したら初期値設定
0133:         } catch (xml_parser_errore) {
0134:             initParameter();
0135:             return;
0136:         }
0137:     //読み込み失敗したら初期値設定
0138:     } catch (xml_parser_errore) {
0139:         initParameter();
0140:         return;
0141:     }
0142: }

0144: /**
0145:  * パラメータの保存
0146:  * @param なし
0147:  * @return なし
0148:  */
0149: void saveParameter(void) {
0150:     //ブラウザ・コントロールからパラメータを取り出す
0151:     wstring val;
0152:     if (wBrowser.getInputById(L"longitude", &val)) {
0153:         Longitude = stod(val);
0154:     }
0155:     if (wBrowser.getInputById(L"latitude", &val)) {
0156:         Latitude = stod(val);
0157:     }
0158:     if (wBrowser.getInputById(L"zoom", &val)) {
0159:         Zoom = stoi(val);
0160:     }
0161:     if (wBrowser.getInputById(L"maptype", &val)) {
0162:         Maptype = _WS(val);
0163:     }
0164: 
0165:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
0166:     snprintf(lngSIZE_BUFF, "%.5f", Longitude);
0167:     snprintf(latSIZE_BUFF, "%.5f", Latitude);
0168: 
0169:     //XMLファイルへ書き込む
0170:     ptree pt;
0171:     ptreechild1 = pt.add("parameter.param", lat);
0172:     child1.add("<xmlattr>.type", "latitude");
0173:     ptreechild2 = pt.add("parameter.param", lng);
0174:     child2.add("<xmlattr>.type", "longitude");
0175:     ptreechild3 = pt.add("parameter.param", to_string(Zoom));
0176:     child3.add("<xmlattr>.type", "zoom");
0177:     ptreechild4 = pt.add("parameter.param", Maptype);
0178:     child4.add("<xmlattr>.type", "maptype");
0179: 
0180:     const int indent = 4;
0181:     write_xml(getMyPath(APPNAME) + APPNAME + ".xml", ptstd::locale(),
0182:         xml_writer_make_settings<std::string>(' ', indent));
0183: }

前回状態の保持を目的として、地図の中心点(緯度・経度)、表示倍率、マップタイプを XML ファイルに保存し、次回起動するときにそれを読み込むようにした。
その他の関数、マップ描画、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header