C++ で最寄駅を検索

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

(2022年9月23日)UserAgent追加,ウィンドウ位置保存,ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
stationsearchwin.msiインストーラ
bin/stationsearchwin.exe実行プログラム本体
bin/cwebpage.dll
bin/libcurl.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ブラウザ・クラス(ヘッダ)
sour/makefileビルド

使用ライブラリ

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

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

リソースの準備

今回は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 にすること。

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

解説:定数など

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

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

解説:検索の流れ

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

0893:         //検索
0894:         case IDM_EXEC:
0895:         case IDC_BUTTON_EXEC:
0896:             //検索キーがあればジオコードAPI呼び出し
0897:             Query = getStrEditBox(hDlgIDC_EDIT_QUERY);
0898:             if (Query.length() > 0) {
0899:                 if (pGC->searchPoints(_SW(Query), UserAgent, 0) > 0) {
0900:                     Longitude = pGC->Ppoints[0].longitude;
0901:                     Latitude  = pGC->Ppoints[0].latitude;
0902:                 }
0903:             //検索キーがなければ地図中心座標を取り出す
0904:             } else {
0905:                 if (wBrowser.getInputById(L"longitude", &val)) {
0906:                     Longitude = stod(val);
0907:                 }
0908:                 if (wBrowser.getInputById(L"latitude", &val)) {
0909:                     Latitude = stod(val);
0910:                 }
0911:             }
0912:             if (wBrowser.getInputById(L"zoom", &val)) {
0913:                 Zoom = stoi(val);
0914:             }
0915:             if (wBrowser.getInputById(L"maptype", &val)) {
0916:                 Maptype = _WS(val);
0917:             }
0918:             //カーソルを砂時計に
0919:             SetCursor(LoadCursor(NULLIDC_WAIT));
0920:             //最寄駅検索
0921:             cnt = pHR->getResults_Heartrails(LatitudeLongitude);
0922:             if (cnt > 0) {
0923:                 cnt = makeInformation(cnt);
0924:             }
0925:             //ブラウザ・コントロールに表示
0926:             viewBrowser(tmpnamecnt);
0927:             //最寄駅の一覧表示
0928:             makeListView(GetDlgItem(hDlgIDC_LISTVIEW_STATIONS));
0929:             //エラー・リセット
0930:             ErrorMessage = "";
0931:             pGC->resetError();
0932:             break;

解説:INPUT要素の取得

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

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

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

0081: /**
0082:  * IES(InternetExplorer_Server)のハンドルを返す
0083:  * @param   なし
0084:  * @return  HWND ハンドル
0085: */
0086: HWND getIES(void) {
0087:     TCHAR className[128];
0088:     HWND hChild = GetWindow(controlGW_CHILD);
0089:     while (hChild) {
0090: //      std::cout << "hWnd = " << hChild << std::endl;
0091:         GetClassName(hChildclassNamesizeof(className));
0092: //      std::wcout << "className = " << className << std::endl;
0093:         if (_tcscmp(_T("Internet Explorer_Server"), className) == 0) {
0094:             return hChild;
0095:         }
0096:         hChild = GetWindow(hChildGW_CHILD);
0097:     }
0098:     return NULL;
0099: }

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

HeartRails Geo API 最寄駅検索

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

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

解説:HeartRails Geo APIの呼び出し

0365: /**
0366:  * HeartRails Express API から必要な情報を配列に格納する
0367:  * @param   double latitude  緯度(世界測地系)
0368:  * @param   double longitude 経度(世界測地系)
0369:  * @return  int ヒット数/(-1):エラー発生
0370: */
0371: int pahooHeartRailsExpress::getResults_Heartrails(double latitudedouble longitude) {
0372:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
0373: 
0374:     snprintf(lngSIZE_BUFF, "%.5f", longitude);
0375:     snprintf(latSIZE_BUFF, "%.5f", latitude);
0376:     this->webapi = "http://express.heartrails.com/api/xml?method=getStations&x=" + (string)lng + "&y=" + (string)lat;
0377:     static string contents = "";
0378:     bool res = readWebContents(this->webapiUserAgent, &contents);
0379:     if (res == FALSE) {
0380:         this->errmsg = _SW("HeartRails Express APIの接続エラーが発生しました");
0381:         return (-1);
0382:     }
0383: 
0384:     //配列の初期化
0385:     for (int i = 0; i < __SIZE_PPOINTSi++) {
0386:         this->Pstations[i].id = 0;
0387:         this->Pstations[i].title = this->Pstations[i].line = this->Pstations[i].distance = L"";
0388:         this->Pstations[i].latitude = this->Pstations[i].longitude = 0.0;
0389:     }
0390: 
0391:     //XML読み込み
0392:     int cnt = 0;
0393:     try {
0394:         std::stringstream ss;
0395:         ss << contents;
0396:         ptree pt;
0397:         xml_parser::read_xml(sspt);
0398: 
0399:         //応答チェック
0400:         if (optional<string>str = pt.get_optional<string>("response.station")) {
0401:         } else {
0402:             this->errmsg = _SW("HeartRails Geo APIの応答エラーが発生しました");
0403:             return (-1);
0404:         }
0405: 
0406:         //XML解釈
0407:         try {
0408:             for (auto it : pt.get_child("response")) {
0409:                 if (cnt >= __SIZE_PPOINTS) {
0410:                     break;
0411:                 }
0412:                 //読み飛ばし
0413:                 if (it.first != "station") {
0414:                     continue;
0415:                 }
0416:                 //最寄駅名
0417:                 if (optional<string>name = it.second.get_optional<string>("name")) {
0418:                     this->Pstations[cnt].title = _UW(name.value());
0419:                 }
0420:                 //緯度
0421:                 if (optional<string>lat = it.second.get_optional<string>("y")) {
0422:                     this->Pstations[cnt].latitude = stod(lat.value());
0423:                 }
0424:                 //経度
0425:                 if (optional<string>lng = it.second.get_optional<string>("x")) {
0426:                     this->Pstations[cnt].longitude = stod(lng.value());
0427:                 }
0428:                 //路線
0429:                 if (optional<string>line = it.second.get_optional<string>("line")) {
0430:                     this->Pstations[cnt].line = _UW(line.value());
0431:                 }
0432:                 //距離
0433:                 if (optional<string>distance = it.second.get_optional<string>("distance")) {
0434:                     this->Pstations[cnt].distance = _UW(distance.value());
0435:                 }
0436:                 //識別子
0437:                 this->Pstations[cnt].id = {(char)(65 + cnt)};
0438:                 cnt++;
0439:             }
0440:         //XML解釈エラー
0441:         } catch(ptree_bad_pathe) {
0442:             this->errmsg = _SW("HeartRails Geo APIの検索エラーが発生しました");
0443:             return (-1);
0444:         }
0445: 
0446:     //読み込みエラー
0447:     } catch(xml_parser_errore) {
0448:         this->errmsg = _SW("HeartRails Geo APIの接続エラーが発生しました");
0449:         return (-1);
0450:     }
0451:     contents.clear();
0452: 
0453:     return cnt;
0454: }

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

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

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

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

参考サイト

(この項おわり)
header